Skip to Content
ConventionsState Management Rules

State Management Rules

RNCopilot uses a two-layer state architecture. Server/async state is managed by React Query. Client/UI state is managed by Zustand. These two layers have clear responsibilities and never overlap.

Two-Layer Architecture

LayerLibraryResponsibility
Server state@tanstack/react-queryData from APIs, loading states, caching, background refetching
Client stateZustandUser preferences, auth session, UI toggles, local-only state

Do not use React Context for state management. There is no AuthContext or AuthProvider in this project. Auth is a Zustand store.

Zustand Store Pattern

Every Zustand store follows the same structure: a state interface, initial state, and actions.

// src/features/myFeature/stores/myFeatureStore.ts import { create } from 'zustand'; import type { MyItem } from '../types'; interface MyFeatureState { // State items: MyItem[]; selectedId: string | null; // Actions setItems: (items: MyItem[]) => void; selectItem: (id: string | null) => void; addItem: (item: MyItem) => void; removeItem: (id: string) => void; reset: () => void; } const initialState = { items: [], selectedId: null, }; export const useMyFeatureStore = create<MyFeatureState>((set) => ({ ...initialState, setItems: (items) => set({ items }), selectItem: (id) => set({ selectedId: id }), addItem: (item) => set((state) => ({ items: [...state.items, item] })), removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })), reset: () => set(initialState), }));

Always Use Selectors

Never subscribe to the entire Zustand store. Always use a selector to pick only what the component needs. Subscribing to the whole store causes unnecessary re-renders.

// CORRECT -- subscribes only to what is needed const items = useMyFeatureStore((s) => s.items); const addItem = useMyFeatureStore((s) => s.addItem); // WRONG -- subscribes to all state, re-renders on any change const store = useMyFeatureStore();

Auth Store

Auth is managed exclusively by useAuthStore in src/providers/auth/authStore.ts. This is the only way to read or modify auth state.

Reading Auth State in Components

import { useAuthStore } from '@/providers/auth/authStore'; const user = useAuthStore((s) => s.user); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const isLoading = useAuthStore((s) => s.isLoading);

Accessing Auth Outside React

In non-React code (e.g., Axios interceptors), use getState():

const session = useAuthStore.getState().session;

Never create a React Context for auth. Never manage session state locally in a component. All auth flows go through useAuthStore.

React Query Conventions

Query Key Factory Pattern

Use factory functions for query keys, not inline arrays. This enables precise cache invalidation.

// src/features/products/hooks/useProducts.ts export const PRODUCT_KEYS = { all: ['products'] as const, list: (filters?: object) => [...PRODUCT_KEYS.all, 'list', filters] as const, detail: (id: string) => [...PRODUCT_KEYS.all, 'detail', id] as const, };

Query Hooks

Place all query and mutation hooks in src/features/<feature>/hooks/:

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/services/api'; import type { Product, CreateProductData } from '../types'; export function useProducts(filters?: object) { return useQuery({ queryKey: PRODUCT_KEYS.list(filters), queryFn: () => api.get<Product[]>('/products', { params: filters }) .then((r) => r.data), }); } export function useProduct(id: string) { return useQuery({ queryKey: PRODUCT_KEYS.detail(id), queryFn: () => api.get<Product>(`/products/${id}`).then((r) => r.data), enabled: !!id, }); } export function useCreateProduct() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (data: CreateProductData) => api.post<Product>('/products', data).then((r) => r.data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: PRODUCT_KEYS.all }); }, }); }

Rules Summary

  • One query key factory per feature, exported as a const object.
  • Use enabled to conditionally run queries (e.g., enabled: !!id).
  • Invalidate via the factory: queryClient.invalidateQueries({ queryKey: PRODUCT_KEYS.all }).
  • Place API calls in queryFn directly or extract them into service files under src/features/<feature>/services/.
Last updated on