236

GitHub - react-spring/zustand: 🐻 Bear necessities for state management in React

 2 years ago
source link: https://github.com/react-spring/zustand
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

readme.md

bear.png

Bundle Size Build Status npm version npm

Small, fast and scaleable bearbones state-management solution. Has a comfy api based on hooks, that isn't boilerplatey or opinionated, but still just enough to be explicit and flux-like. Try a small live demo here.

npm install zustand

First create a store

Your store is a hook! You can put anything in it, atomics, objects, functions. Like Reacts setState, set merges state.

import create from 'zustand'

const [useStore] = create(set => ({
  count: 0,
  increase: () => set(state => ({ count: state.count + 1 })),
  reset: () => set({ count: 0 })
}))

Then bind your components, that's it!

Use the hook anywhere, no providers needed. Once you have selected state your component will re-render on changes.

function Counter() {
  const count = useStore(state => state.count)
  return <h1>{count}</h1>
}

function Controls() {
  const increase = useStore(state => state.increase)
  return <button onClick={increase}>up</button>
}

Why zustand over react-redux?

  • Simpler and un-opinionated
  • Makes hooks the primary means of consuming state
  • Doesn't wrap your app into context providers
  • Can inform components transiently (without causing render)

Recipes

Fetching everything

You can, but remember that it will cause the component to update on every state change!

const state = useStore()

Selecting multiple state slices

zustand defaults to strict-equality (old === new) to detect changes, this is efficient for atomic state picks.

const foo = useStore(state => state.foo)
const bar = useStore(state => state.bar)

If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can tell zustand that you want the object to be diffed shallowly by passing an alternative equality function.

import shallow from 'zustand/shallow'

// Object pick, re-renders the component when either foo or bar change
const { foo, bar } = useStore(state => ({ foo: state.foo, bar: state.bar }), shallow)

// Array pick, re-renders the component when either foo or bar change
const [foo, bar] = useStore(state => [state.foo, state.bar], shallow)

// Mapped picks, re-renders the component when state.objects changes in order, count or keys
const keys = useStore(state => Object.keys(state.objects), shallow)

Fetching from multiple stores

Since you can create as many stores as you like, forwarding results to succeeding selectors is as natural as it gets.

const currentUser = useCredentialsStore(state => state.currentUser)
const person = usePersonStore(state => state.persons[currentUser])

Memoizing selectors

Selectors run on state changes, as well as when the component renders. If you give zustand a fixed reference it will only run on state changes, or when the selector changes. Don't worry about this, unless your selector is expensive.

const fooSelector = useCallback(state => state.foo[props.id], [props.id])
const foo = useStore(fooSelector)

Async actions

Just call set when you're ready, it doesn't care if your actions are async or not.

const [useStore] = create(set => ({
  json: {},
  fetch: async url => {
    const response = await fetch(url)
    set({ json: await response.json() })

Read from state in actions

set allows fn-updates set(state => result), but you still have access to state outside of it through get.

const [useStore] = create((set, get) => ({
  text: "hello",
  action: () => {
    const text = get().text

Sick of reducers and changing nested state? Use Immer!

Reducing nested structures is tiresome. Have you tried immer?

import produce from "immer"

const [useStore] = create(set => ({
  nested: { structure: { contains: { a: "value" } } },
  set: fn => set(produce(fn)),
}))

const set = useStore(state => state.set)
set(state => void state.nested.structure.contains = null)

Reading/writing state and reacting to changes outside of components

You can use it with or without React out of the box.

const [, api] = create(() => ({ a: 1, b: 2, c: 3 }))

// Getting fresh state
const a = api.getState().a
// Listening to all changes, fires on every dispatch
const unsub1 = api.subscribe(state => console.log("state changed", state))
// Listening to selected changes
const unsub2 = api.subscribe(a => console.log("a changed", a), state => state.a)
// Updating state, will trigger listeners
api.setState({ a: 1 })
// Unsubscribe listeners
unsub1()
unsub2()
// Destroying the store (removing all listeners)
api.destroy()

Transient updates (for often occuring state-changes)

The api signature of subscribe(callback, selector):unsub allows you to easily bind a component to a store without forcing it to re-render on state changes, you will be notified in a callback instead. Best combine it with useEffect for automatic unsubscribe on unmount. This can make a drastic performance difference when you are allowed to mutate the view directly.

const [useStore, api] = create(set => ({ "0": [-10, 0], "1": [10, 5], ... }))

function Component({ id }) {
  // Fetch initial state
  const xy = useRef(api.getState()[id])
  // Connect to the store on mount, disconnect on unmount, catch state-changes in a callback
  useEffect(() => api.subscribe(coords => (xy.current = coords), state => state[id]), [id])

Middleware

You can functionally compose your store any way you like.

// Log every time state is changed
const log = config => (set, get, api) => config(args => {
  console.log("  applying", args)
  set(args)
  console.log("  new state", get())
}, get, api)

// Turn the set method into an immer proxy
const immer = config => (set, get, api) => config(fn => set(produce(fn)), get, api)

const [useStore] = create(log(immer(set => ({
  text: "hello",
  setText: input => set(state => {
    state.text = input
  })
}))))

Can't live without redux-like reducers and action types?

const types = { increase: "INCREASE", decrease: "DECREASE" }

const reducer = (state, { type, by = 1 }) => {
  switch (type) {
    case types.increase: return { count: state.count + by }
    case types.decrease: return { count: state.count - by }
  }
}

const [useStore] = create(set => ({
  count: 0,
  dispatch: args => set(state => reducer(state, args)),
}))

const dispatch = useStore(state => state.dispatch)
dispatch({ type: types.increase, by: 2 })

Or, just use our redux-middleware. It wires up your main-reducer, sets initial state, and adds a dispatch function to the state itself and the vanilla api. Try this example.

import { redux } from 'zustand/middleware'

const [useStore] = create(redux(reducer, initialState))

Redux devtools

import { devtools } from 'zustand/middleware'

// Usage with a plain action store, it will log actions as "setState"
const [useStore] = create(devtools(store))
// Usage with a redux store, it will log full action types
const [useStore] = create(devtools(redux(reducer, initialState)))

devtools takes the store function as its first argument, optionally you can name the store with a second argument: devtools(store, "MyStore"), which will be prefixed to your actions.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK