npm install zustand
// 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,
})),
}))
);
// 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.
// 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),
}))
);
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.
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 ✅
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.