As React applications grow in complexity, managing state becomes increasingly important. Zustand is a small, fast, scalable state management library for React which provides a simple and efficient way to manage the state, reducing boilerplate code and improving performance compared to traditional state management solutions like Redux.
One of the key features of Zustand is that changes to the state trigger re-renders in components subscribed to the store, eliminating the need for manually managing state updates with useEffect hooks.
In this blog post, we'll explore how Zustand works and how you can use it in your React projects, especially when working with TypeScript.
Getting Started with Zustand
To get started with Zustand, you'll need to install it in your project:
npm install zustand
Next, we'll create a file called store.ts
where we'll define our Zustand store. A store is essentially a container for your application's state and the functions to update that state.
How Zustand Handles State Updates
In Zustand, the set
function is called with a function that takes the current state (state
) as an argument and returns the new state. This approach ensures that state updates are based on the current state and allows for side effects, like updating localStorage
, to be performed within the state update logic.
Creating a Store
We'll start by defining a CounterStore
interface to specify the shape of our store. We then use the create
function provided by Zustand to create a store with an initial state of { counter: 0 }
and two functions, increment
and reset
, to update the counter
state. Here's a simple example of creating a store:
import { create } from 'zustand';
interface CounterStore {
counter: number;
increment: () => void;
reset: () => void;
}
const useCounterStore = create<CounterStore>((set) => ({
counter: 0,
increment: () => set((state) => ({ counter: state.counter + 1 })),
reset: () => set(() => ({ counter: 0 })),
}));
export default useCounterStore;
Using Zustand in React Components
Once you've created a store, you can use it anywhere in your React components using the useStore
hook. Select your state and the consuming component will re-render when that state changes.
import useCounterStore from './store';
const Counter = () => {
const counter = useCounterStore((s) => s.counter);
const increment = useCounterStore((s) => s.increment);
return (
<div>
Counter {counter}
<button onClick={() => increment()}>Increment</button>
</div>
);
};
export default Counter;
In this component, we use the useStore
hook to access the counter
state and the increment
functions from our store.
Usage in other components:
import useCounterStore from './store';
const NavBar = () => {
const counter = useCounterStore((s) => s.counter);
const reset = useCounterStore((s) => s.reset);
return (
<nav>
<span>Navbar items: {counter} </span>
<button onClick={() => reset()}>Reset Counter</button>
</nav>
);
};
export default NavBar;
Advanced Store Examples
AuthStore
Here's an example of an AuthStore for managing user authentication:
import { create } from 'zustand';
interface AuthStore {
user: string;
login: (username: string) => void;
logout: () => void;
}
const useAuthStore = create<AuthStore>((set) => ({
user: '',
login: (username) => set(() => ({ user: username })),
logout: () => set(() => ({ user: '' })),
}));
export default useAuthStore;
GameQueryStore
Here's an example of a GameQueryStore for managing game queries:
import { create } from 'zustand';
interface GameQuery {
genreId?: number;
platformId?: number;
sortOrder?: string;
searchText?: string;
}
interface GameQueryStore {
gameQuery: GameQuery;
setGenreId: (genreId: number) => void;
setPlatformId: (platformId: number) => void;
setSortOrder: (sortOrder: string) => void;
setSearchText: (searchText: string) => void;
}
const useGameQueryStore = create<GameQueryStore>((set) => ({
gameQuery: {},
setSearchText: (searchText) => set(() => ({ gameQuery: { searchText } })),
setGenreId: (genreId) =>
set((store) => ({ gameQuery: { ...store.gameQuery, genreId } })),
setPlatformId: (platformId) =>
set((store) => ({ gameQuery: { ...store.gameQuery, platformId } })),
setSortOrder: (sortOrder) =>
set((store) => ({ gameQuery: { ...store.gameQuery, sortOrder } })),
}));
export default useGameQueryStore;
LikesStore
Here's an example of a GameQueryStore for managing game queries:
import { create } from 'zustand';
import { Photo } from './hooks/useImages';
interface LikesState {
likedImages: Photo[];
addLikedImage: (image: Photo) => void;
removeLikedImage: (id: string) => void;
}
const useLikesStore = create<LikesState>((set) => ({
likedImages: JSON.parse(localStorage.getItem('likedImages') || '[]'),
addLikedImage: (image) =>
set((state) => {
const newLikedImages = [...state.likedImages, image];
localStorage.setItem('likedImages', JSON.stringify(newLikedImages));
return { likedImages: newLikedImages };
}),
removeLikedImage: (id) =>
set((state) => {
const newLikedImages = state.likedImages.filter(
(image) => image.id !== id
);
localStorage.setItem('likedImages', JSON.stringify(newLikedImages));
return { likedImages: newLikedImages };
}),
}));
export default useLikesStore;
In this example, we define a Zustand store called useLikesStore
that manages a list of liked images. The addLikedImage
and removeLikedImage
functions allow us to add and remove images from the list, respectively, while updating the localStorage
to persist the changes.
Conclusion
Zustand is a great choice for managing the global state in your React applications. By using Zustand, we can simplify state management in our application and ensure that changes to the state trigger re-renders in components that are subscribed to the store. Give Zustand a try in your next React project and experience the benefits for yourself!
Happy Coading!