6

Are both ways of fetching states from the Zustand store identical in respect of re-renders on state changes?

Method described in the documentation:

const nuts = useStore(state => state.nuts)
const honey = useStore(state => state.honey)

Shorthand:

const { nuts, honey } = useStore()
wenzf
  • 337
  • 6
  • 14

2 Answers2

15

Short Answer

No, these methods are not identical.

Long Answer

As mentioned in the zustand readme:

Fetching everything

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

const state = useStore()

So, When you select certain slices of state using a selector, the component will only update/rerender when the selected values change.

When you call useStore() without passing any parameters to it, you are effectively subscribing your component to the whole state. As a metaphor, you could say that "zustand will be asking the component to update/rerender on any state change, anywhere in the state tree". Object destructuring in the shorthand syntax is just syntactic sugar to assign variables to object properties in a quicker way. The value that useStore() returns (and subscribes the component to) is still the whole state.

So, if you use const { nuts, honey } = useStore(), you may run into performance issues. Whether those issues will be noticeable or not depends on the app, but I'd say it's easy enough to just use selectors and not have to worry about it.

Further suggestion

If you need to select all the slices of state in a single useStore(...) call, the recommended method is to use suitable selectors for that. Quoting from Selecting multiple state slices

import shallow from 'zustand/shallow'

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

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

// Mapped picks, re-renders the component when state.treats changes in order, count or keys
const treats = useStore(state => Object.keys(state.treats), shallow)
YSK
  • 528
  • 4
  • 7
  • 2
    i love zustand but man this syntax is pretty rough to use, especially since "shallow" doesn't like to be auto imported in VSCode – ICW Jun 20 '22 at 18:16
3

No, they are not identical: the first will re-render only when the selected properties change; and the second will re-render whenever any property of the store changes.

The motivation for the question seems to be to avoid the verbosity of fetching multiple states with Zustand (as ICW also commented, this can be a pain.)

You can achieve a more concise syntax for multiple zustand imports in a couple of different ways.

Option 1

Let's define this helper function:

function useMulti(useFunc, ...items) {
  return items.reduce((carry, item) => ({
    ...carry,
    [item]: useFunc(state => state[item]),
  }), {})
}

After defining this wrapper function, instead of writing this:

const {nuts, honey} = useStore(state => ({
  nuts: state.nuts,
  honey: state.honey,
}), shallow)

you can write this:

const {nuts, honey} = useMulti(useStore, 'nuts', 'honey')

The wrapper function is just providing some syntactic sugar for selecting multiple states.

  • Note that internally, useMulti uses a separate useStore call for each asked-for prop. Therefore, state invalidation is equivalent to using zustand/shallow without useMulti.
  • Also note that useMulti only works for top-level properties of the store: you would need a more complicated useMulti implementation if you want to fetch state.x.y.z, for example.

If you don't like the functional call style of useMulti(useStore(...)), you can leverage useMulti() to create a wrapper function for useStore() specifically:

export const useStoreMulti = (...items) => useMulti(useStore, ...items)

This allows you to write

const {nuts, honey} = useStoreMulti('nuts', 'honey')

Option 2

It can be cleaner to just import them separately. This is particularly true if you're selecting many different store props: consider the following, where zustand is quite verbose:

const {
  nuts,
  honey,
  salmon,
  berries,
  fruit,
  insects,
  carrion,
  newbornElk,
  roots,
  garbage,
} = useStore(state => ({
  nuts: state.nuts,
  honey: state.honey,
  salmon: state.salmon,
  berries: state.berries,
  fruit: state.fruit,
  insects: state.insects,
  carrion: state.carrion,
  newbornElk: state.newbornElk,
  roots: state.roots,
  garbage: state.garbage,
}), shallow)

vs this, which is broadly equivalent:

const nuts = useStore(state => state.nuts)
const honey = useStore(state => state.honey)
const salmon = useStore(state => state.salmon)
const berries = useStore(state => state.berries)
const fruit = useStore(state => state.fruit)
const insects = useStore(state => state.insects)
const carrion = useStore(state => state.carrion)
const newbornElk = useStore(state => state.newbornElk)
const roots = useStore(state => state.roots)
const garbage = useStore(state => state.garbage)

One way to reduce the number of characters per call here is to define a local helper:

const localUse = (prop) => useStore(state => state[prop])

const nuts = localUse('nuts')
const honey = localUse('honey')
const salmon = localUse('salmon')
const berries = localUse('berries')
const fruit = localUse('fruit')
const insects = localUse('insects')
const carrion = localUse('carrion')
const newbornElk = localUse('newbornElk')
const roots = localUse('roots')
const garbage = localUse('garbage)
Benji XVI
  • 2,192
  • 1
  • 25
  • 24