Create a new React Context with Provider and useReducer for state management following best practices.
What to Create:
- •
Ask the user:
- •Context name (e.g., "User", "Theme", "Cart")
- •Which feature does it belong to? (or "shared" for global context)
- •What state does it manage? (list of state fields with types)
- •Example: "currentUser (User | null), isAuthenticated (boolean), loading (boolean)"
- •What actions are needed? (list of actions with payload types)
- •Example: "LOGIN (User), LOGOUT (void), UPDATE_USER (Partial<User>), SET_LOADING (boolean)"
- •
Generate Context in
src/features/[feature]/context/[ContextName]Context.tsx(orsrc/features/shared/context/[ContextName]Context.tsxfor global):- •State interface with TypeScript types
- •Action discriminated union types
- •Initial state constant
- •Reducer function with switch statement
- •Context with createContext
- •Provider component wrapping children
- •Custom useContext hook with error checking (throws if used outside provider)
- •All exports are named (no default exports)
- •Proper TypeScript typing (no
any)
- •
Generate Tests in
src/features/[feature]/context/[ContextName]Context.test.tsx:- •Import from
@testing-library/react - •Reducer tests:
- •Test each action updates state correctly
- •Test initial state
- •Test invalid actions return unchanged state
- •Provider tests:
- •Test provider renders children
- •Test initial state is accessible
- •Hook tests (using renderHook):
- •Test hook provides state and dispatch
- •Test dispatching actions updates state
- •Test error when hook used outside provider
- •Use
describeblocks to organize tests - •Aim for 100% coverage (critical state management)
- •Import from
Context Structure Pattern:
// [ContextName]Context.tsx
import { createContext, useContext, useReducer, type ReactNode } from 'react';
// State interface
export interface [ContextName]State {
field1: Type1;
field2: Type2;
// ... more fields
}
// Action types (discriminated union)
export type [ContextName]Action =
| { type: 'ACTION_1'; payload: PayloadType1 }
| { type: 'ACTION_2'; payload: PayloadType2 }
| { type: 'ACTION_3' } // No payload
| { type: 'ACTION_4'; payload: PayloadType4 };
// Context type
interface [ContextName]ContextType {
state: [ContextName]State;
dispatch: React.Dispatch<[ContextName]Action>;
}
// Create context
const [ContextName]Context = createContext<[ContextName]ContextType | undefined>(
undefined
);
// Initial state
const initialState: [ContextName]State = {
field1: defaultValue1,
field2: defaultValue2,
// ... more fields
};
// Reducer function
function [contextName]Reducer(
state: [ContextName]State,
action: [ContextName]Action
): [ContextName]State {
switch (action.type) {
case 'ACTION_1':
return {
...state,
field1: action.payload,
// Update relevant fields
};
case 'ACTION_2':
return {
...state,
field2: action.payload,
};
case 'ACTION_3':
return {
...state,
// Update without payload
};
case 'ACTION_4':
return {
...state,
field3: action.payload,
};
default:
return state;
}
}
// Provider component
export function [ContextName]Provider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer([contextName]Reducer, initialState);
return (
<[ContextName]Context.Provider value={{ state, dispatch }}>
{children}
</[ContextName]Context.Provider>
);
}
// Custom hook
export function use[ContextName]Context() {
const context = useContext([ContextName]Context);
if (!context) {
throw new Error('use[ContextName]Context must be used within [ContextName]Provider');
}
return context;
}
Test Structure Pattern:
// [ContextName]Context.test.tsx
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import {
[ContextName]Provider,
use[ContextName]Context,
type [ContextName]State,
type [ContextName]Action,
} from './[ContextName]Context';
describe('[ContextName]Context', () => {
describe('Reducer', () => {
it('returns initial state', () => {
const { result } = renderHook(() => use[ContextName]Context(), {
wrapper: [ContextName]Provider,
});
expect(result.current.state).toEqual({
field1: defaultValue1,
field2: defaultValue2,
// ... expected initial state
});
});
it('handles ACTION_1', () => {
const { result } = renderHook(() => use[ContextName]Context(), {
wrapper: [ContextName]Provider,
});
act(() => {
result.current.dispatch({ type: 'ACTION_1', payload: testValue });
});
expect(result.current.state.field1).toBe(testValue);
});
it('handles ACTION_2', () => {
const { result } = renderHook(() => use[ContextName]Context(), {
wrapper: [ContextName]Provider,
});
act(() => {
result.current.dispatch({ type: 'ACTION_2', payload: testValue });
});
expect(result.current.state.field2).toBe(testValue);
});
// Add more action tests...
});
describe('Provider', () => {
it('renders children', () => {
render(
<[ContextName]Provider>
<div>Test Child</div>
</[ContextName]Provider>
);
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
it('provides state and dispatch', () => {
const { result } = renderHook(() => use[ContextName]Context(), {
wrapper: [ContextName]Provider,
});
expect(result.current.state).toBeDefined();
expect(result.current.dispatch).toBeDefined();
expect(typeof result.current.dispatch).toBe('function');
});
});
describe('use[ContextName]Context hook', () => {
it('throws error when used outside provider', () => {
// Suppress console.error for this test
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => use[ContextName]Context());
}).toThrow('use[ContextName]Context must be used within [ContextName]Provider');
spy.mockRestore();
});
it('provides context value when used inside provider', () => {
const { result } = renderHook(() => use[ContextName]Context(), {
wrapper: [ContextName]Provider,
});
expect(result.current).toBeDefined();
expect(result.current.state).toBeDefined();
expect(result.current.dispatch).toBeDefined();
});
it('allows multiple actions in sequence', () => {
const { result } = renderHook(() => use[ContextName]Context(), {
wrapper: [ContextName]Provider,
});
act(() => {
result.current.dispatch({ type: 'ACTION_1', payload: value1 });
result.current.dispatch({ type: 'ACTION_2', payload: value2 });
});
expect(result.current.state.field1).toBe(value1);
expect(result.current.state.field2).toBe(value2);
});
});
});
Real-World Example (UserContext):
Based on the user asking for "User" context managing "currentUser (User | null), isAuthenticated (boolean)" with actions "LOGIN (User), LOGOUT (void), UPDATE_USER (Partial<User>)":
// UserContext.tsx
import { createContext, useContext, useReducer, type ReactNode } from 'react';
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
export interface UserState {
currentUser: User | null;
isAuthenticated: boolean;
}
export type UserAction =
| { type: 'LOGIN'; payload: User }
| { type: 'LOGOUT' }
| { type: 'UPDATE_USER'; payload: Partial<User> };
interface UserContextType {
state: UserState;
dispatch: React.Dispatch<UserAction>;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
const initialState: UserState = {
currentUser: null,
isAuthenticated: false,
};
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case 'LOGIN':
return {
...state,
currentUser: action.payload,
isAuthenticated: true,
};
case 'LOGOUT':
return {
...state,
currentUser: null,
isAuthenticated: false,
};
case 'UPDATE_USER':
return {
...state,
currentUser: state.currentUser
? { ...state.currentUser, ...action.payload }
: null,
};
default:
return state;
}
}
export function UserProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(userReducer, initialState);
return (
<UserContext.Provider value={{ state, dispatch }}>
{children}
</UserContext.Provider>
);
}
export function useUserContext() {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUserContext must be used within UserProvider');
}
return context;
}
Code Standards Compliance:
Follow all standards from CODE_STANDARDS.md:
- •React Standards: Functional components, named exports, hooks rules
- •TypeScript Standards: Strict mode, no
any, proper typing, discriminated unions - •Testing Standards: 100% coverage for state management (critical logic)
- •Naming Conventions:
- •PascalCase for Context, Provider, State interface
- •camelCase for reducer function
- •UPPER_SNAKE_CASE for action types
- •use prefix for custom hook
Architecture Patterns:
Follow the Context Layer pattern from ARCHITECTURE.md:
- •Use
useReducerfor complex state (multiple related state values) - •Separate context creation from provider
- •Provide custom hook for consuming context
- •Type state and actions properly
- •Export state, action types, provider, and hook
File Organization:
For feature-specific context:
src/features/[feature]/context/ ├── [ContextName]Context.tsx └── [ContextName]Context.test.tsx
For global/shared context:
src/features/shared/context/ ├── [ContextName]Context.tsx └── [ContextName]Context.test.tsx
After Creation:
- •Verify both files are created in correct locations
- •Run tests:
npm test [ContextName]Context.test.tsx - •Verify 100% test coverage
- •Check TypeScript compilation:
npm run type-check - •Update feature's
index.tsto export:tsxexport { [ContextName]Provider, use[ContextName]Context } from './context/[ContextName]Context'; export type { [ContextName]State, [ContextName]Action } from './context/[ContextName]Context';
When to Use Context:
- •Feature Context: State shared across multiple components within a feature
- •Global Context: State needed by multiple features (auth, theme, i18n)
- •Use Local State: If state is only used in one component, use
useStateinstead
Best Practices:
- •Keep reducer functions pure (no side effects)
- •Use discriminated unions for type-safe actions
- •Always provide error message in custom hook
- •Test all actions thoroughly
- •Document complex state transitions
- •Consider using action creators for complex payloads
- •Keep context focused (single responsibility)
Be concise and follow React Context best practices. Ask clarifying questions if state structure or actions are unclear.