This is a simple example which I created to revise the working of Zustand with React and TypeScript with the help of the official Documentation of Zustand
Zustand Official Docs
Installing Zustand using npm
npm install zustand
Defining the types for the values inside the store
We are defining what the type of the state will be and the actions will be so that TypeScript understands while checking it.
type State = { firstName: string, lastName: string, age: number,
} type Action = { updateFirstName: (firstName: State['firstName']) => void; updateLastName: (lastName: State['lastName']) => void; updateAge: () => void;
}
Creation of a store
A Store is where you initialize the default values for the state and define the actions stating how it should work an manipulate the state (the data set inside the store)
const usePersonStore = create<State & Action>((set) => ({ firstName: '', lastName: '', age: 0, updateAge: () => set((state) => ({ age: state.age + 1 })), updateFirstName: (firstName) => set(() => ({ firstName: firstName })), updateLastName: (lastName) => set(() => ({ lastName: lastName })), }))
Usage of this simple store in React
//COMPONENT 1
export default function App() { //we use the state of the store to access the variables stored inside it. const firstName = usePersonStore((state) => state.firstName) const updateFirstName = usePersonStore((state) => state.updateFirstName) const age = usePersonStore((state) => state.age); return <> <label htmlFor="firstName"> First Name <input type="text" name="firstName" id="firstName" onChange={(e) => updateFirstName(e.target.value)} /> </label> <AgeComp /> <p> Hello, <strong>{firstName}</strong> and your age is <strong>{age}</strong> </p> </>
} //COMPONENT 2
const AgeComp = () => { const updateAge = usePersonStore((state) => state.updateAge) return <> <Button onClick={updateAge}>+</Button> </>
}
The Final code in a single file will look like this
import { create } from "zustand";
import { Button } from "./components/button";
type State = { firstName: string, lastName: string, age: number,
} type Action = { updateFirstName: (firstName: State['firstName']) => void; updateLastName: (lastName: State['lastName']) => void; updateAge: () => void;
} const usePersonStore = create<State & Actions>((set) => ({ firstName: '', lastName: '', age: 0, updateAge: () => set((state) => ({ age: state.age + 1 })), updateFirstName: (firstName) => set(() => ({ firstName: firstName })), updateLastName: (lastName) => set(() => ({ lastName: lastName })), })) const AgeComp = () => { const updateAge = usePersonStore((state) => state.updateAge) return <> <Button onClick={updateAge}>+</Button> </>
} export default function App() { const firstName = usePersonStore((state) => state.firstName) const updateFirstName = usePersonStore((state) => state.updateFirstName) const age = usePersonStore((state) => state.age); return <> <label htmlFor="firstName"> First Name <input type="text" name="firstName" id="firstName" onChange={(e) => updateFirstName(e.target.value)} /> </label> <AgeComp /> <p> Hello, <strong>{firstName}</strong> and your age is <strong>{age}</strong> </p> </>
}import { create } from "zustand";
import { Button } from "./components/button";
type State = { firstName: string, lastName: string, age: number,
} type Action = { updateFirstName: (firstName: State['firstName']) => void; updateLastName: (lastName: State['lastName']) => void; updateAge: () => void;
} const usePersonStore = create<State & Actions>((set) => ({ firstName: '', lastName: '', age: 0, updateAge: () => set((state) => ({ age: state.age + 1 })), updateFirstName: (firstName) => set(() => ({ firstName: firstName })), updateLastName: (lastName) => set(() => ({ lastName: lastName })), })) const AgeComp = () => { const updateAge = usePersonStore((state) => state.updateAge) return <> <Button onClick={updateAge}>+</Button> </>
} export default function App() { const firstName = usePersonStore((state) => state.firstName) const updateFirstName = usePersonStore((state) => state.updateFirstName) const age = usePersonStore((state) => state.age); return <> <label htmlFor="firstName"> First Name <input type="text" name="firstName" id="firstName" onChange={(e) => updateFirstName(e.target.value)} /> </label> <AgeComp /> <p> Hello, <strong>{firstName}</strong> and your age is <strong>{age}</strong> </p> </>
}
Creating Slices in zustand
This is to split up a store which can potentially scale up and become huge in the future. So we split up a huge store into slices to achieve modularity.
Creating types for the individual slices
Create a new file inside src/store/fish-bear-slice.ts and start adding the below code.
type BearSlice = { bears: number; addBear: () => void; eatFish: () => void; resetBears: () => void;
}; type FishSlice = { fishes: number; addFish: () => void; resetFishes: () => void;
}; type SharedSlice = { addBoth: () => void; getBoth: () => void; resetBoth: () => void;
};
Creating the slices using StateCreator
const createBearSlice: StateCreator< BearSlice & FishSlice, [], [], BearSlice
> = (set) => ({ bears: 0, addBear: () => set((state) => ({ bears: state.bears + 1 })), eatFish: () => set((state) => ({ fishes: state.fishes - 1 })), resetBears: () => set(() => ({ bears: 0 })),
}); const createFishSlice: StateCreator< BearSlice & FishSlice, [], [], FishSlice
> = (set) => ({ fishes: 0, addFish: () => set((state) => ({ fishes: state.fishes + 1 })), resetFishes: () => set(() => ({ fishes: 0 })),
});
Creating a shared slice
Combining the above given two individual slices to one.This will let you use every single action and state used in the store as one yet manage all the slices modularly.
const createSharedSlice: StateCreator< BearSlice & FishSlice, [], [], SharedSlice
> = (set, get) => ({ addBoth: () => { get().addBear(); get().addFish(); }, getBoth: () => get().bears + get().fishes, resetBoth: () => { get().resetBears(); get().resetFishes(); },
});
Creating a combined Store using a custom hook.
We are exporting this custom hook so that we can use it in the react component.
export const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()( (...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createSharedSlice(...a), })
);
Using the above created combined slice in React
import { useBoundStore } from "./store/fish-bear-slice" const Controllers = () => { const addBear = useBoundStore((state) => state.addBear) const addFish = useBoundStore((state) => state.addFish) const addBoth = useBoundStore((state) => state.addBoth) const resetBoth = useBoundStore((state) => state.resetBoth) return <> <button onClick={() => addBear()}>Add a bear</button> <button onClick={() => addFish()}>Add a fish</button> <button onClick={() => addBoth()}>Add both</button> <button onClick={() => resetBoth()}>Reset Both</button> </>
} function App() { const bears = useBoundStore((state) => state.bears) const fishes = useBoundStore((state) => state.fishes) return ( <div> <h2>Number of bears: {bears}</h2> <h2>Number of fishes: {fishes}</h2> <Controllers /> </div> )
} export default App
Persisting the data inside the state using the persist middleware from zustand
The above code will work perfectly but once you refresh the page the data is not stored anywhere and will reset the data to its default value. To retain the data even after we reload we use the help of the ‘persist’ middleware from zustand and provide it with a unique localStorage name.
_Persist utilizes the localStorage to persist the data and zustand handles the saving and retrieving part of the job.
_
Import the persist from zustand/middleware
import { persist } from "zustand/middleware";
Add the persist middleware to the combined slice.
Do not use it on individual slices since it will create a problem at the end.
export const useBoundStore = create<BearSlice & FishSlice & SharedSlice>()( persist( (...a) => ({ ...createBearSlice(...a), ...createFishSlice(...a), ...createSharedSlice(...a), }), { name: "bound-store" } )
);