108
LVL 04 — SENIOR-IN-TRAININGSESSION 108DAY 108

ZUSTAND STATE

🎫 PIXELCRAFT-094
🔧Refactor | 🟡 Medium | Priority: 🟡 Medium

Our useReducer + Context state management causes entire subtrees to re-render when any state changes. The editor lags when adjusting sliders. Extract editor state to Zustand. Compare performance. Learn when Context isn't enough and what the alternatives are.
CONCEPTS.UNLOCKED
🐻
Zustand
Lightweight state management — ~1KB. No providers, no boilerplate. Create a store with create(), subscribe with a hook. Components only re-render when their selected slice changes. Simple API, powerful results.
⚠️
When Context Isn't Enough
Context re-renders ALL consumers on ANY change. Change brightness → every component reading EditorContext re-renders (toolbar, canvas, sidebar, settings). With Zustand: only the brightness slider re-renders.
🍕
Slices Pattern
Split a large store into focused slices. editorSlice, authSlice, uiSlice — each manages its own state and actions. Compose them into one store. Organized, testable, scalable.
🔍
DevTools
See state changes in real-time. Zustand integrates with Redux DevTools: time-travel debugging, action logging, state inspection. Same debugging power, much less boilerplate.
💾
Persistence Middleware
Save state to localStorage automatically. User preferences, recent colors, tool settings — persist across sessions. Zustand's persist middleware handles serialization and hydration.
HANDS-ON.TASKS
01
Install Zustand
npm install zustand
02
Create the Editor Store
// stores/editorStore.ts import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; interface Filters { brightness: number; contrast: number; saturation: number; blur: number; } interface EditorState { // State image: ImageData | null; filters: Filters; activeTool: string; history: ImageData[]; historyIndex: number; // Actions setImage: (img: ImageData) => void; setFilter: ( name: keyof Filters, value: number ) => void; setTool: (tool: string) => void; undo: () => void; redo: () => void; pushHistory: (img: ImageData) => void; } export const useEditorStore = create<EditorState>()( devtools((set, get) => ({ image: null, filters: { brightness: 50, contrast: 50, saturation: 50, blur: 0, }, activeTool: 'select', history: [], historyIndex: -1, setImage: (image) => set({ image }), setFilter: (name, value) => set((state) => ({ filters: { ...state.filters, [name]: value, }, })), setTool: (tool) => set({ activeTool: tool }), undo: () => { const { history, historyIndex } = get(); if (historyIndex > 0) set({ historyIndex: historyIndex - 1, image: history[historyIndex - 1], }); }, redo: () => { const { history, historyIndex } = get(); if (historyIndex < history.length - 1) set({ historyIndex: historyIndex + 1, image: history[historyIndex + 1], }); }, pushHistory: (img) => set((state) => ({ history: [ ...state.history.slice( 0, state.historyIndex + 1), img], historyIndex: state.historyIndex + 1, })), })) );
03
Selective Subscriptions
// Only re-renders when brightness // changes — NOT when tool changes function BrightnessSlider() { const brightness = useEditorStore( (s) => s.filters.brightness); const setFilter = useEditorStore( (s) => s.setFilter); return ( <input type="range" value={brightness} onChange={(e) => setFilter('brightness', +e.target.value)} /> ); } // Only re-renders when tool changes function ToolSelector() { const activeTool = useEditorStore( (s) => s.activeTool); const setTool = useEditorStore( (s) => s.setTool); // ... } // Context: BOTH re-render on ANY // state change. // Zustand: each re-renders ONLY // when its selected data changes.
This is the key advantage: selector-based subscriptions. Each component subscribes to exactly the data it needs. Zustand uses Object.is comparison by default — no unnecessary re-renders.
04
Slices Pattern
// stores/slices/uiSlice.ts interface UISlice { sidebarOpen: boolean; theme: 'light' | 'dark'; toggleSidebar: () => void; setTheme: (t: 'light'|'dark') => void; } const createUISlice = ( set: any ): UISlice => ({ sidebarOpen: true, theme: 'dark', toggleSidebar: () => set((s: any) => ({ sidebarOpen: !s.sidebarOpen })), setTheme: (theme) => set({ theme }), }); // Compose slices: const useStore = create()( devtools((...a) => ({ ...createEditorSlice(...a), ...createUISlice(...a), ...createAuthSlice(...a), })) );
05
Persist Middleware
import { persist } from 'zustand/middleware'; const usePrefsStore = create()( persist( (set) => ({ recentColors: [] as string[], lastTool: 'select', addColor: (color: string) => set((s) => ({ recentColors: [ color, ...s.recentColors.slice(0, 9) ], })), }), { name: 'pixelcraft-prefs', // ↑ localStorage key } ) ); // Close browser → reopen → // preferences are still there. // Zero code for serialization.
06
Close the Ticket
git switch -c refactor/PIXELCRAFT-094-zustand git add src/stores/ git commit -m "Migrate state to Zustand: editor, UI, prefs (PIXELCRAFT-094)" git push origin refactor/PIXELCRAFT-094-zustand # PR → Review → Merge → Close ticket ✅
CS.DEEP-DIVE

The state management spectrum.

There is no "best" state library — only the right tool for your complexity level. Understanding when to graduate from one level to the next is the real skill.

// State management spectrum:

useState  (component state)
  Simple, local, one component
  Use for: form inputs, toggles

useReducer (complex component)
  Action-based, predictable
  Use for: complex local logic

Context   (shared state)
  Provider pattern, no lib needed
  Use for: auth, theme, locale
  ⚠️ Re-renders all consumers

Zustand   (global state)
  Selectors, minimal re-renders
  Use for: editor, complex UI

Redux     (enterprise state)
  Middleware, time-travel, ecosystem
  Use for: very large apps, teams

// Zustand: the sweet spot for most
// apps. Redux power, useState DX.
"State Lab"
[A]Profile the re-render count: add React DevTools Profiler. Adjust a brightness slider 10 times. Count re-renders with Context vs Zustand. Document the difference — it should be dramatic.
[B]Add subscribe for external systems: useEditorStore.subscribe() to sync state to the Web Worker (from session 092). When filters change, automatically post a message to the worker. No React re-render needed.
[C]Research: compare Zustand, Jotai, and Recoil. Zustand is store-based (outside React). Jotai is atom-based (inside React). Recoil is graph-based. When would you choose each? Write a comparison.
REF.MATERIAL
ARTICLE
Pmndrs
Official Zustand guide: creating stores, selectors, middleware, devtools, persistence. The simplest state management with the best DX.
ZUSTANDOFFICIALESSENTIAL
VIDEO
Jack Herrington
Practical Zustand tutorial: stores, selectors, slices, devtools, and comparison with Redux. Real-world patterns.
ZUSTANDTUTORIAL
ARTICLE
Pmndrs
How to split stores into focused slices and compose them. Organize large stores without losing simplicity.
ZUSTANDPATTERNS
ARTICLE
React Team
Official React state guide: when to use useState, useReducer, and Context. The foundation before reaching for external libraries.
REACTSTATEOFFICIAL
VIDEO
Theo (t3.gg)
The current state management landscape: Zustand vs Jotai vs Redux vs signals. When to use each and why Zustand won.
STATECOMPARISON
// LEAVE EXCITED BECAUSE
Editor state migrated to Zustand. Slider adjustments are buttery smooth — only the slider component re-renders, not the entire page. DevTools show every state change. The right tool for the right problem.