State store
Zustand and Redux — files and usage, separated by Vite/React vs Next.
Choose None, Zustand, or Redux Toolkit. Patterns below are split by stack.
Zustand
Vite SPA & React Router
File injected:
textsrc/app/store/useCounterStore.ts
src/app/store/useCounterStore.tsNo provider — import the hook anywhere. Persist key: react-dose-counter-storage.
tsxexample.tsx
import { useCounterStore } from "@/app/store/useCounterStore";
export function Counter() {
const count = useCounterStore((s) => s.count);
const increment = useCounterStore((s) => s.increment);
return (
<button type="button" onClick={increment}>
{count}
</button>
);
}Next.js
Same store file and API as Vite. Still no provider — it is a client hook with localStorage persist. Use it from Client Components.
tsxCounter.tsx
"use client";
import { useCounterStore } from "@/app/store/useCounterStore";
export function Counter() {
const count = useCounterStore((s) => s.count);
const increment = useCounterStore((s) => s.increment);
return <button onClick={increment}>{count}</button>;
}Redux Toolkit
Vite SPA & React Router
Files injected:
text
src/app/store/store.ts
src/app/store/hooks.ts
src/app/store/slices/counterSlice.ts
src/app/providers/redux-provider.tsxSingleton store. Nested in RootProvider as <ReduxProvider>.
tsxexample.tsx
import { useAppDispatch, useAppSelector } from "@/app/store/hooks";
import { increment } from "@/app/store/slices/counterSlice";
export function Counter() {
const count = useAppSelector((s) => s.counter.count);
const dispatch = useAppDispatch();
return (
<button type="button" onClick={() => dispatch(increment())}>
{count}
</button>
);
}Next.js
Same file set, but store creation is Next-safe:
makeStore()instead of a module singleton- Client
redux-providerkeeps one store inuseRef - Still nested under
RootProvider
Usage of useAppDispatch / useAppSelector is the same in Client Components.