This episode practices project preparation: creating a React project, installing zustand, organizing the stores folder structure, and writing your first counter store. You also learn the pattern of reading state via selectors and the limitations of destructuring the entire store.

Theory alone isn't enough — in episode 3 you will create a real project and write your first store. We'll set up a React project with Vite, install Zustand, build the stores folder structure, then write a fully working counter store. From here on, every upcoming episode uses the same pattern.
The focus of this episode is habits: a clean folder structure, correct selectors, and avoiding common beginner traps. You'll carry these patterns into your production project later in episode 21.
If you haven't completed episode 0 yet, set up the project first:
npm create vite@latest belajar-zustand -- --template react-ts
cd belajar-zustand
npm install
npm i zustandThe command npm i zustand adds the core library. Make sure package.json shows version 5.x for zustand — if it's still 4.x, run npm i zustand@latest to upgrade it.
One architecture decision from the start: all stores live in src/stores/, one file per store. This rule lets your team know exactly where to find state.
src/
├── components/
├── pages/
└── stores/
├── counter.ts
└── (store lain di sini)Create the src/stores/ folder now. Store files are named in lowercase in this project, and each store is exported as a hook prefixed with use.
Type the following code in src/stores/counter.ts:
import { create } from 'zustand'
type CounterState = {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounter = create<CounterState>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
reset: () => set({ count: 0 }),
}))set((s) => ({ count: s.count + 1 })) uses an updater function that receives the current state. set({ count: 0 }) uses a plain object directly. Both forms are valid — the difference between them is broken down in episode 4.
Open src/App.tsx and use the store:
import { useCounter } from './stores/counter'
export default function App() {
const count = useCounter((s) => s.count)
const increment = useCounter((s) => s.increment)
const decrement = useCounter((s) => s.decrement)
const reset = useCounter((s) => s.reset)
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={increment}>Tambah</button>
<button onClick={decrement}>Kurang</button>
<button onClick={reset}>Reset</button>
</div>
)
}useCounter((s) => s.count) reads state; useCounter((s) => s.increment) fetches the action function. Both are called as separate hooks so that each call only subscribes to one slice.
Use one useStore per slice, as above. A more concise alternative is using useShallow for several slices in a single object — we'll discuss that in episode 5.
The most common beginner trap is returning the entire store from a selector:
// BAD: entire store, triggers a re-render on every change
const counter = useCounter()
const { count, increment } = counterThe pattern above makes the component subscribe to the entire store, so selective re-rendering is lost. useCounter() without a selector is the doorway to the performance problems discussed in episode 13.
Run the dev server:
npm run devOpen http://localhost:5173 — the Tambah, Kurang, and Reset buttons must work without a reload. If they do, your first store is alive.
The patterns you applied in episode 3:
src/stores/, one file per store.useXxx hook resulting from create().useCounter((s) => s.count).useCounter((s) => s.increment).These small habits determine the quality of your state architecture in large projects.
Episode 3 completes the practical foundation: a Vite project with zustand installed, the src/stores/ folder structure, a typed counter store, and a component using it with per-slice selectors. You also recognized a pattern to avoid — destructuring the entire store.
Key takeaways:
src/stores/ and export them as hooks prefixed with use.npm i zustand is enough to start; verify version 5.x in package.json.set accepts an updater function or a plain object.npm run dev to verify the store works in the browser.In the next episode we will break down the create(), set, and get API — the complete store anatomy, the two forms of set, the role of get for reading the current state, and the correct update patterns with immutability. Your first store is just one of many patterns to come.