DEV Community

Cover image for Using Zustand with React JS! ๐Ÿš€
Franklin Martinez
Franklin Martinez

Posted on

Using Zustand with React JS! ๐Ÿš€

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.

๐Ÿ“Œ What is Zustand?

๐Ÿ“Œ Advantages of using Zustand.

๐Ÿ“Œ Creating the project.

๐Ÿ“Œ Creating a store.

๐Ÿ“Œ Accessing the store.

๐Ÿ“Œ Accessing multiple states.

๐Ÿ“Œ Why do we use the shallow function?

๐Ÿ“Œ Updating the state.

๐Ÿ“Œ Creating an action.

๐Ÿ“Œ Accessing the state stored in the store.

๐Ÿ“Œ Executing the action.

๐Ÿ“Œ Conclusion.

ย 

๐Ÿš€ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).

code .
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ Creating a store.

First we must install Zustand:

npm install zustand
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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( () => ({

}));
Enter fullscreen mode Exit fullscreen mode

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 
}));
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ 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
Enter fullscreen mode Exit fullscreen mode

โญ• 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"
}));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
  )
Enter fullscreen mode Exit fullscreen mode

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
}));
Enter fullscreen mode Exit fullscreen mode

โญ• 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 ) => {}
}));
Enter fullscreen mode Exit fullscreen mode

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 }),
}));
Enter fullscreen mode Exit fullscreen mode

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 }))
Enter fullscreen mode Exit fullscreen mode

You can also do asynchronous actions as follows and that's it!

updateAmount: async(newAmount: number ) => {
    // to do fetching data
    set({ amount: newAmount })
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก 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,

Enter fullscreen mode Exit fullscreen mode

โญ• 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  }))
    },
}));
Enter fullscreen mode Exit fullscreen mode

โญ• 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
Enter fullscreen mode Exit fullscreen mode

๐Ÿš€ 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)

Collapse
 
latobibor profile image
Andrรกs Tรณth

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 at redux-saga).

Collapse
 
franklin030601 profile image
Franklin Martinez

That really interests me, thank you! ๐Ÿ™Œ

Collapse
 
stianhave profile image
Stian Hรฅve

isnt this a better way to get the properties from the store?

const {amount, updateAmount} = useBookStore()
Enter fullscreen mode Exit fullscreen mode

instead of

const amount = useBookStore(state => state.amount)
const updateAmount = useBookStore(state => state.updateAmount)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ccreusat profile image
Clรฉment Creusat • Edited

Best ways to avoid unnecessary rerenders with zustand are :

Direct selectors:

const amount = useBookStore(state => state.amount)
const updateAmount = useBookStore(state => state.updateAmount)
Enter fullscreen mode Exit fullscreen mode

Object destructuring with shallow:

import { shallow } from "zustand/shallow";

const { amount } = useBookStore((state) => ({
    amount: state.amount,
  }), shallow);
Enter fullscreen mode Exit fullscreen mode

Since v4.x.x, you can also use the useShallow hook:

import { useShallow } from 'zustand/react/shallow'

const { amount } = useBookStore(
    useShallow((state) => ({ amount: state.amount })),
);
Enter fullscreen mode Exit fullscreen mode

Other ways are wrong (unless re-renders / performance is not a problem) :

const state = useBookStore(); 
Enter fullscreen mode Exit fullscreen mode
const { amount } = useBookStore();
Enter fullscreen mode Exit fullscreen mode
const { amount } = useBookStore((state) => ({
    amount: state.amount,
}});
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mohajerimasoud profile image
Masoud Mohajeri

no
if any other changes occur in store other than amount and updateAmount , you will have unnecessary rerender .

Collapse
 
stianhave profile image
Stian Hรฅve

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.

Collapse
 
joshnwosu profile image
Joshua Nwosu

This isn't true.

Thread Thread
 
stianhave profile image
Stian Hรฅve

Can you elaborate?

Collapse
 
flash010603 profile image
Usuario163

incredible post I find it very interesting and useful

Collapse
 
avi_developer profile image
Avi Nash

Fantastic tutorial.