Skip to content
🐻 Bear necessities for state management in React
TypeScript JavaScript
Branch: master
Clone or download
Latest commit 2c8908e Aug 15, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src fix #53 Aug 15, 2019
tests Rerun selector on error Jun 28, 2019
.babelrc.js Rerun selector on error Jun 28, 2019
.gitignore Add TypeScript Apr 18, 2019
.npmignore fix typo Apr 9, 2019
.size-snapshot.json fix #53 Aug 15, 2019
.travis.yml add travis May 12, 2019
LICENSE fix typo Apr 9, 2019
bear.png update bear Apr 10, 2019
package.json 1.0.4 Aug 15, 2019
readme.md Update readme.md Aug 13, 2019
rollup.config.js #41 devtools prefix, ship shallowequal in its own export Jun 30, 2019
tsconfig.json Add TypeScript Apr 18, 2019
yarn.lock Bump lodash from 4.17.11 to 4.17.14 Jul 13, 2019

readme.md

Build Status npm version npm

Small, fast and scaleable bearbones state-management solution. Has a comfy api based on hooks, isn't that 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'

const { foo, bar } = useStore(state => ({ foo: state.foo, bar: state.bar }), 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), {
  selector: 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([selector,] callback):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. 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), { selector: 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.

You can’t perform that action at this time.