Managing state is a must in modern React JS applications. That's why today I will give you an introduction to "Zustand" a popular alternative to manage your status in your applications.
Any kind of feedback is welcome, thank you and I hope you enjoy the article.🤗
🚨 Note: This post requires you to know the basics of React with TypeScript.
Table of Contents.
📌 Creating the project.
📌 Creating a store.
📌 Accessing the store.📌 Creating an action.
📌 Accessing the state stored in the store.
📌 Executing the action.
🚀 What is Zustand?
Zustand is a small, fast and scalable status management solution. Its state management is centralized and action-based.
Zustand was developed by Jotai and React-spring's creators.
You can use Zustand in both React and some other technology like Angular, Vue JS or even vanilla JavaScript.
Zustand is an alternative to other state managers like Redux, Jotai Recoil, etc.
⭕ Advantages of using Zustand.
- Less repeated code (compared to Redux).
- Easy to understand documentation.
- Flexibility
- You can use Zustand the simple way, with TypeScript, you can integrate immer for immutability or you can even write code similar to the Redux pattern (reducers and dispatch).
- It does not wrap the application in a provider as is commonly done in Redux.
- Renders components only when there are changes.
🚀 Creating the project.
We will name the project: zustand-tutorial
(optional, you can name it whatever you like).
npm init vite@latest
We create the project with Vite JS and select React with TypeScript.
Then we run the following command to navigate to the directory just created.
cd zustand-tutorial
Then we install the dependencies.
npm install
Then we open the project in a code editor (in my case VS code).
code .
🚀 Creating a store.
First we must install Zustand:
npm install zustand
Once the library is installed, we need to create a folder src/store
and inside the folder we add a new file called bookStore.ts
and inside this file, we will create our store.
First we import the zustand package and name it bookStore.ts
. create
import create from 'zustand';
Then we create a constant with the name useBookStore (this is because zustand uses hooks underneath and in its documentation it names the stores this way).
To define the store we use the create function.
import create from 'zustand';
export const useBookStore = create();
The create function takes a callback function as a parameter, which returns an object, to create the store.
import create from 'zustand';
export const useBookStore = create( () => ({
}));
For better autocompletion, we will use an interface to define the properties of our store, as well as the functions.
Then we set the initial value of the properties, in this case the amount property will initially be 40.
import create from 'zustand';
interface IBook {
amount: number
}
export const useBookStore = create<IBook>( () => ({
amount: 40
}));
🚀 Accessing the store.
To access our store, we need to import our store.
In our src/App.tsx
file we import our store.
Without using providers like in Redux, we can use our store almost anywhere ("almost" because it follows the rules of hooks, since the store is basically a hook underneath).
Basically we call to our hook, like any other, only that by parameter we must indicate it by means of a callback that property we want to obtain of the store and thanks to the autocomplete it helps us a lot.
import { useBookStore } from './store/bookStore';
const App = () => {
const amount = useBookStore(state => state.amount)
return (
<div>
<h1>Books: {amount} </h1>
</div>
)
}
export default App
⭕ Accessing multiple states.
Suppose you have more than one state in your store, for example, let's add the title:
import create from 'zustand';
interface IBook {
amount: number
author: string
}
export const useBookStore = create<IBook>( () => ({
amount: 40,
title: "Alice's Adventures in Wonderland"
}));
To access more states we could do the following:
Case 1 - One way is individually, go accessing the state, creating new constants.
import { useBookStore } from './store/bookStore';
const App = () => {
const amount = useBookStore(state => state.amount)
const title = useBookStore(state => state.title)
return (
<div>
<h1>Books: {amount} </h1>
</div>
)
}
export default App
Case 2 - But if you want, you can also create a single object with multiple states or properties. And to tell Zustand to diffuse the object shallowly, we must pass the shallow function.
import shallow from 'zustand/shallow'
import { useBookStore } from './store/bookStore';
const App = () => {
const { amount, title } = useBookStore(
(state) => ({ amount: state.amount, title: state.title }),
shallow
)
return (
<div>
<h1>Books: {amount} </h1>
<h4>Title: {title} </h4>
</div>
)
}
export default App
Although it would be better to place the store in a separate hook if it grows too much in terms of properties.
In both case 1 and case 2 the components will be rendered when the title and amount change.
🔴 Why do we use the shallow function?
In the above case where we access several states of the store, we use the shallow function, why?
By default if we do not use shallow, Zustand detects changes with strict equality (old === new), which is efficient for atomic states.
const amount = useBookStore(state => state.amount)
But in case 2, we are not obtaining an atomic state, but an object (the same happens if we use an array).
const { amount, title } = useBookStore(
(state) => ({ amount: state.amount, title: state.title }),
shallow
)
So the default strict equality would not be useful in this case to evaluate objects and always triggering a re-render even if the object does not change.
So Shallow will upload the object/array and compare its keys, if any is different it will recreate again and trigger a new render.
🚀 Updating the state.
To update the state in the store we must do it by creating new properties in src/store/bookStore.ts
adding functions to update modify the store.
In the callback that receives the create function, this function receives several parameters, the first one is the set function, which will allow us to update the store.
export const useBookStore = create<IBook>(( set ) => ({
amount: 40
}));
⭕ Creating an action.
First we create a new property to update the amount and it will be called updateAmount which receives a number as parameter.
import create from 'zustand'
interface IBook {
amount: number
updateAmount: (newAmount: number) => void
}
export const useBookStore = create<IBook>((set) => ({
amount: 40,
updateAmount: (newAmount: number ) => {}
}));
In the body of the updateAmount function we execute the set function by sending an object, referencing the property to be updated.
import create from 'zustand'
interface IBook {
amount: number
updateAmount: (newAmount: number) => void
}
export const useBookStore = create<IBook>( (set) => ({
amount: 40,
updateAmount: (newAmount: number ) => set({ amount: newAmount }),
}));
The set function can also receive a function as a parameter, which is useful to get the previous state.
Optionally I scatter the whole state (assuming I have more properties) and only update the state I need, in this case the amount.
Note: Spreading properties should also be taken into account when your states are objects or arrays that are constantly changing.
updateAmount: (newAmount: number ) => set( state => ({ ...state, amount: state.amount + newAmount }))
You can also do asynchronous actions as follows and that's it!
updateAmount: async(newAmount: number ) => {
// to do fetching data
set({ amount: newAmount })
}
💡 Note: the set function accepts a second boolean parameter, default is false. Instead of merging, it will replace the state model. You must be careful not to delete important parts of your store such as actions.
updateAmount: () => set({}, true), // clears the entire store, actions included,
⭕ Accessing the state stored in the store.
To define the state we use the set function, but what if we want to get the values of the state?
Well for that we have the second parameter next to set, which is get() that gives us access to the state.
import create from 'zustand'
interface IBook {
amount: number
updateAmount: (newAmount: number) => void
}
export const useBookStore = create<IBook>( (set, get) => ({
amount: 40,
updateAmount: (newAmount: number ) => {
const amountState = get().amount
set({ amount: newAmount + amountState })
//is the same as:
// set(state => ({ amount: newAmount + state.amount }))
},
}));
⭕ Executing the action.
To execute the action, it is simply to access the property as we have done previously. And we execute it, sending the necessary parameters, that in this case is only a number.
import { useBookStore } from './store/bookStore';
const App = () => {
const amount = useBookStore(state => state.amount)
const updateAmount = useBookStore(state => state.updateAmount)
return (
<div>
<h1> Books: {amount} </h1>
<button
onClick={ () => updateAmount(10) }
> Update Amount </button>
</div>
)
}
export default App
🚀 Conclusion.
Zustand provides easy access and update of status, which makes it a friendly alternative to other status managers.
In my personal opinion, Zustand has pleased me a lot for its above mentioned features, it is one of my favorite libraries to manage status, as well as Redux Toolkit. You should definitely give it a try to use it in some project 😉.
I hope I have helped you to better understand how this library works and how to use it, thank you very much for coming this far! 🤗
I invite you to comment if you know of any other important features of Zustand or best practices for code. 🙌
Top comments (10)
isnt this a better way to get the properties from the store?
instead of
Best ways to avoid unnecessary rerenders with zustand are :
Direct selectors:
Object destructuring with
shallow
:Since v4.x.x, you can also use the
useShallow
hook:Other ways are wrong (unless re-renders / performance is not a problem) :
no
if any other changes occur in store other than amount and updateAmount , you will have unnecessary rerender .
Oh wow, had no clue. Thanks for pointing that out!
I guess it would still work with smaller and specific stores though, but i'll keep it in mind.
This isn't true.
Can you elaborate?
Check out OvermindJS. It looks like it is the more fleshed out version of Zustand, but with
immer
already integrated.We use it in two of our React projects and we never looked back at
redux
(especially not atredux-saga
).That really interests me, thank you! 🙌
incredible post I find it very interesting and useful
Fantastic tutorial.