💾 Archived View for wilw.capsule.town › log › 2021-02-05-react-state-zustand.gmi captured on 2023-09-08 at 16:15:41. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-04-19)
-=-=-=-=-=-=-
React state management is what gives the library its reactiveness. It's what makes it so easy to build performant data-driven applications that dynamically update based on the underlying data. In this example the app would automatically update the calculation result as the user types in the input boxes:
import React, { useState } from 'react'; function MultiplicationCalculator() { const [number1, setNumber1] = useState(0); const [number2, setNumber2] = useState(0); return ( <> <input value={number1} onChange={e => setNumber1(parseInt(e.target.value))} /> <input value={number2} onChange={e => setNumber2(parseInt(e.target.value))} /> <p>The result is {number1 * number2}.</p> </> ); }
The entire function will re-run on each state change (the `setNumber1` and `setNumber2` functions) in order to reactively update the result text. The multiplication itself could be calculated in a `useEffect` but it is simpler to look at it as shown.
This is totally fine for many apps, however this quickly becomes unmanageable when you need to share state (e.g. `number1`) between this component and another component - and ensure that a state change in the former can be reflected in the latter - whether it's an ancestor, descendant, or a more distant component. Of course, you can pass the state variables (and the associated `setState` functions) from a parent down as `props` to child components, but as soon as you're doing this more than a handful of times or in cases where state needs to be shared across distant components this quickly becomes hard to maintain or understand.
An example of shared state might be to store the details about the currently logged-in user in an app. A navigation bar component would need to know about the user state to show a link to the correct profile page, and another component may need access to the same state in order to allow the user to change their name.
This is by no means a new problem. Many of these issues are solved using React's Context API [1] and there are also libraries like Redux that are useful in perhaps more complex scenarios - it's much more opinionated and involves a fair bit of extra code that may be overkill in many apps. Adding just a small piece of state (e.g. a new text input), and the ability to alter it, to Redux involves updating reducers, creating an action, dispatchers, and wiring things through to your components using `connect`, `mapStateToProps`, and `mapDispatchToProps`. Plus you'll need the relevant provider higher up.
Redux is certainly a fantastic library, however, and I use it in many apps. This post [2] is useful and discusses the cases in which you may (or may not) want to use Redux.
In this post I want to talk about another option that is perhaps quicker and easier to use, expecially for those newer to React (though it's also great for more seasoned React developers) - zustand [3]. Not only is this the German word for "state", it's also a nice and succinct library for state management for React.
The zustand library is pretty concise, so you shouldn't need to add too much extra code. To get started just add it as a dependency to your project (e.g. `yarn add zustand`). Now let's rewrite the earlier multiplication example but using zustand.
First, define a _store_ for your app. This will contain all of the values you want to keep in your global state, as well as the functions that allow those values to change (_mutators_). In our store, we'll extract out the state for `number1` and `number2` we used in our component from earlier, and the appropriate update functions (e.g. `setNumber1`), into the store:
import React from 'react'; import create from 'zustand'; const useStore = create((set) => ({ number1: 0, number2: 0, setNumber1: (x) => set(() => ({ number1: x })), setNumber2: (x) => set(() => ({ number2: x })), }));
Now - in the same file - we can go ahead and rewrite our component such that it now uses this store instead of its own local state:
function MultiplicationCalculator() { const { number1, number2, setNumber1, setNumber2 } = useStore(); return ( <> <input value={number1} onChange={e => setNumber1(parseInt(e.target.value))} /> <input value={number2} onChange={e => setNumber2(parseInt(e.target.value))} /> <p>The result is {number1 * number2}.</p> </> ); }
That's it - we now have a React app that uses zustand. As before, the component function runs each time the store's state changes, and zustand ensures things are kept up-to-date.
In the example above the two blocks of code are in the same file. However, the power of zustand becomes particularly useful when the store is shared amongst several components across different parts of your app to provide "global state".
For example, the `useStore` variable could be declared and exported from a file named `store.js` somewhere in your app's file structure. Then, when a component needs to access its variables or mutator functions it just needs to - for example, `import useStore from 'path/to/store'` - and then use object destructuring [4] (as on line 11 above) to pull out the needed variables and functions.
It's worth checking out the documentation [5] since zustand is super flexible and can be used in ways that help improve performance, such as taking advantage of memoizing and state slicing. It also makes what can be tricky in other such libraries - e.g. asynchronous state updates - trivial.
If you've already got an established app using another state management system it may not be worth migrating everything over. But give zustand a go in your next project if you're looking for straight forward, yet powerful, state management.