managing-state

star 1

Manage frontend state using Zustand for client state, TanStack Query for server state, and React Context for global static values.

7a336e6e By 7a336e6e schedule Updated 2/5/2026

name: managing-state description: Manage frontend state using Zustand for client state, TanStack Query for server state, and React Context for global static values.

Managing State

Goal

Choose the right state management tool for each category of state and implement typed, performant stores that minimize unnecessary re-renders.

When to Use

  • Adding new state to the application (decide where it lives)
  • Migrating from Redux, MobX, or ad-hoc Context stores to Zustand + TanStack Query
  • Optimizing re-renders caused by poorly scoped state

Instructions

1. State Decision Tree

Question Answer
Does the data come from an API? TanStack Query
Is it shared client state across pages? Zustand store
Is it local to one component? useState / useReducer
Is it global and rarely changes (theme, i18n)? React Context

2. Zustand -- Client State

Create a typed store with selectors to prevent full-store re-renders:

// stores/authStore.ts
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";

interface AuthState {
  token: string | null;
  user: { id: string; name: string } | null;
  setAuth: (token: string, user: AuthState["user"]) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  devtools(
    persist(
      (set) => ({
        token: null,
        user: null,
        setAuth: (token, user) => set({ token, user }),
        logout: () => set({ token: null, user: null }),
      }),
      { name: "auth-storage" },
    ),
  ),
);

// Selectors -- consume only the slice you need
export const useToken = () => useAuthStore((s) => s.token);
export const useCurrentUser = () => useAuthStore((s) => s.user);

3. TanStack Query -- Server State

Use queries for reads and mutations for writes. Invalidate related queries after mutations:

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUsers, createUser } from "@/services/userService";
import type { User, CreateUserPayload } from "@/types/user";

export function useUsers() {
  return useQuery<User[]>({
    queryKey: ["users"],
    queryFn: getUsers,
    staleTime: 5 * 60 * 1000,
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (payload: CreateUserPayload) => createUser(payload),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

4. Optimistic Updates

For mutations where instant feedback matters:

export function useUpdateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (user: User) => updateUser(user),
    onMutate: async (updatedUser) => {
      await queryClient.cancelQueries({ queryKey: ["users"] });
      const previous = queryClient.getQueryData<User[]>(["users"]);
      queryClient.setQueryData<User[]>(["users"], (old) =>
        old?.map((u) => (u.id === updatedUser.id ? updatedUser : u)),
      );
      return { previous };
    },
    onError: (_err, _user, context) => {
      queryClient.setQueryData(["users"], context?.previous);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["users"] });
    },
  });
}

5. React Context -- Global Static Values

Use only for values that change infrequently and are needed throughout the tree:

// contexts/ThemeContext.tsx
import { createContext, useContext, useState, type ReactNode } from "react";

type Theme = "light" | "dark";

interface ThemeContextValue {
  theme: Theme;
  toggle: () => void;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");
  const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));
  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>");
  return ctx;
}

Constraints

✅ Do

  • Colocate state as close to its usage as possible
  • Use Zustand selectors to subscribe to individual slices
  • Invalidate TanStack Query caches after mutations
  • Type all store state and actions explicitly
  • Use devtools middleware in Zustand during development

❌ Don't

  • Put server/API data into Zustand -- use TanStack Query instead
  • Use React Context for frequently changing values (causes full subtree re-renders)
  • Create one monolithic global store -- split by domain
  • Call setState directly from inside render (causes infinite loops)
  • Forget to handle loading and error states for queries

Output Format

  • Zustand store files in src/stores/ with exported selectors
  • TanStack Query hooks in src/hooks/ wrapping service functions
  • Context providers in src/contexts/ only for theme, locale, or similar

Dependencies

Install via CLI
npx skills add https://github.com/7a336e6e/skills --skill managing-state
Repository Details
star Stars 1
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator