Determine clear ownership of front-end state to avoid global store bloat, duplicate caching, and derived state synchronization errors.
Don't choose Redux, Zustand, Pinia or Context first. First mark each state to its unique owner, and then decide on the tool.
Status TypeTypical ExampleDefault AttributionLocal UI statePop-up window switch, tab, expand row, hover editing stateIn-component state / refForm statusInput value, dirty field, validation error, submittingForm library or form componentServer statusList, details, paging results, remote errorsRequest cache libraryURL statusSearch terms, filter, sort, page number, selected tabRouting parameters / search paramsGlobal client statusLogin users, topics, permission snapshots, shopping cart draftsGlobal storeBrowser persistent stateDrafts, preferences, offline queues retained across refreshesStorage layer + state adaptertsx
type ReportsStateMap = {
search: "url";
selectedReportId: "url";
reports: "server-state-cache";
isFilterPanelOpen: "local-ui";
draftColumns: "browser-persistence";
};
Do not save a new copy of a value that can be inferred from props, server state, URL, or existing state.
tsx
interface Invoice {
id: string;
status: "draft" | "sent" | "paid";
}
function InvoiceList({ invoices }: { invoices: Invoice[] }) {
const paidInvoices = invoices.filter((invoice) => invoice.status === "paid");
return <span>{paidInvoices.length}</span>;
}
Priority is given to localization and combination in React; the global store is only introduced when cross-page, cross-feature or unified actions are required.
tsx
import { create } from "zustand";
interface WorkspaceState {
activeWorkspaceId: string | null;
setActiveWorkspaceId: (workspaceId: string) => void;
}
export const useWorkspaceStore = create<WorkspaceState>((set) => ({
activeWorkspaceId: null,
setActiveWorkspaceId: (activeWorkspaceId) => set({ activeWorkspaceId }),
}));
Decision order:
useState / useReducer.In Vue 3, use provide/inject for local cross-level transfer; use Pinia or the existing store of the project for global business status.
ts
import { computed, readonly, ref } from "vue";
import { defineStore } from "pinia";
export const useSessionStore = defineStore("session", () => {
const userId = ref<string | null>(null);
const isSignedIn = computed(() => userId.value !== null);
function signIn(nextUserId: string) {
userId.value = nextUserId;
}
return {
userId: readonly(userId),
isSignedIn,
signIn,
};
});
When reconstructing the state, first make a state list, and then gradually move the read and write entries. Each step should keep the behavior verifiable.
ts
interface StateMigrationItem {
name: string;
currentOwner: "component" | "context" | "store" | "query-cache" | "url";
targetOwner: "component" | "context" | "store" | "query-cache" | "url";
verification: string;
}
const migrationPlan: StateMigrationItem[] = [
{
name: "dashboard filters",
currentOwner: "store",
targetOwner: "url",
verification: "refreshing the page preserves filters through search params",
},
];
Load references/state-patterns.md when you need Store shape examples, selector patterns, URL state synchronization, persistence adapters, SSR boundaries, or review checklists.
Output status attribution list, selection reasons, store/API boundaries and verification steps. Key behaviors such as loading/error/empty, refresh, rollback, cross-routing, and permission changes should be retained during implementation.