Have you ever wanted to learn state-management but it felt so overwhelming to get started with Redux? Or maybe you're already using Redux but don't want to take the burden anymore? If so, you might want to take a shot at Zustand! Its an amazing state management library that's easy to get started and maintain in a long run. In this post, we'll dive into creating & sharing stores with this tiny Redux alternative.
Why Zustand?
- Small & Performant
- Beginner friendly
- Rapidly adopted by Frontend devs
In this tutorial, we'll see usage of Zustand directly on React (via Vite). I'll likely add another chapter for using it with Next.js too.
Setup
Fire up your favorite code editor and let's get started! We'll first need to create a fresh project and install the library. I'll be using Node.js as runtime and Bun as package manager throughout the tutorial. You can use whatever suits to you!
$ bun create vite
✔ Project name: … zustand-episode-1
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
Scaffolding project in zustand-episode-1...
Done. Now run:
cd zustand-episode-1
bun install
bun run dev
After you've installed the dependencies, install the Zustand as the following:
bun add zustand
Cool! I've also removed everything inside App.css
, index.css
and App
component:
function App() {
return <></>;
}
export default App;
Creating a Store
What we need first is a store. Store is where we put our states and methods. Its possible to keep primitives, objects and functions in them. Import create
from Zustand and create your own:
import { create } from "zustand";
const useCountStore = create(() => ({ count: 0 }));
function App() {
const count = useCountStore((state) => state.count);
return <>{count}</>;
}
export default App;
Notice that create
takes a callback that returns an object (It is () => ({})
not () => {}
). The object we return is our store. create
function also gives back a hook that we can use to access our store. Inside the App
component, we have used the hook to retrieve the count
.
That's cool but surely we'd like to mutate the count
.
Store Actions
The callback we've passed to create
can optionally take set
argument which is a function that we can use to mutate our state. Modify the useCountStore
hook as the following:
const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
Let's run it down;
-
increment
is a function that callsset
, -
set
takesstate
as an argument which is the current state (similar to set function we get from useState) and returns a new object where the it takesstate.count
and increments by 1, - same applies to decrement but instead it decrements the
count
by 1.
Now that we've created our actions, let's use them in our component to mutate the count
. We can access to our actions like this:
function App() {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);
...
Or, we can take advantage of destructive assignment:
function App() {
const { count, increment, decrement } = useCountStore();
...
Much cleaner! I've also created a barebones interface to play around:
function App() {
const { count, increment, decrement } = useCountStore();
return (
<>
{/* Count value */}
<span>{count}</span>
<br />
{/* Button that increments the count value */}
<button type="button" onClick={increment}>
increase
</button>
{/* Button that decrements the count value */}
<button type="button" onClick={decrement}>
decrease
</button>
</>
);
}
You should be able to see the current count value and increment/decrement it.
Accesing the Store from Another Component
If we're persisting a store outside of our component, we most likely want to access it from some other component. This is also one of the great things about using a state manager, it avoids prop drilling.
Right now our store lives inside App.tsx
. Let's start by moving it to somewhere else. How about a stores
folder inside src
? That sounds like a good idea. Or, we could approach it just like an hook and move it to hooks
folder. I'll go with the former but like I said earlier, whatever suits. My folder structure looks like this now:
node_modules/
public/
src/
stores/
count-store.ts
App.tsx
main.tsx
...
In stores/count-store.ts
, I've took the code for our store and pasted it there. Then I've added export
keyword right before useCountStore
declaration.
stores/count-store.ts
import { create } from "zustand";
export const useCountStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
While at it, we can also add some typing to make our code more understandable:
stores/count-store.ts
import { create } from "zustand";
// States are typed here
interface CountState {
count: number;
}
// Actions (or functions) are typed here
interface CountActions {
increment: () => void;
decrement: () => void;
}
// We create a unified type from them
type CountStore = CountState & CountActions;
// Indicate the type here
export const useCountStore = create<CountStore>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));
This is self-explanatory. If you didn't know, &
in TypeScript merges the types.
Let's also create a new component in src
named Counter.tsx
and fill it in with the following:
src/Counter.tsx
import { useCountStore } from "./stores/count-store";
export default function Counter() {
const count = useCountStore((state) => state.count);
return <span>{count}</span>;
}
Sole purpose of this component is to show count
value, I'm aware that we're already doing it in App
but the idea here is to show that we can access the same store from some other component.
In App.tsx
, import our Counter
component and render it alongside our buttons:
import Counter from "./Counter";
import { useCountStore } from "./stores/count-store";
function App() {
const { increment, decrement } = useCountStore();
return (
<>
{/* Count value */}
<Counter />
<br />
{/* Button that increments the count value */}
<button type="button" onClick={increment}>
increase
</button>
{/* Button that decrements the count value */}
<button type="button" onClick={decrement}>
decrease
</button>
</>
);
}
export default App;
Notice that we're only mutating the count
in App.tsx
. The rendering of count
is moved to Counter.tsx
and it still works perfectly! If our tree would be deeply nested, it still won't be a problem for us to access our store and read/mutate the state.
That's it for part 1. I hope you got the hang of Zustand and liked it! In the follow-up, we'll be trying to integrate React Context API to this and I'll also show you how you can use Zustand in your Next.js apps. Take care!
Top comments (0)