Skip to main content
RapidDev - Software Development Agency
cursor-tutorial

How to generate shared state logic with Cursor

Cursor can generate React Context providers with custom hooks when given clear type definitions and state management patterns. This tutorial shows how to prompt Cursor to create type-safe Context APIs with useReducer, memoized selectors, and provider composition, avoiding the common pitfalls of unnecessary re-renders and loosely typed context values.

What you'll learn

  • How to prompt Cursor to generate typed React Context with custom hooks
  • How to use .cursorrules for consistent state management patterns
  • How to generate Context with useReducer for complex state logic
  • How to avoid re-render issues in Cursor-generated Context providers
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minCursor Free+, React 18+ with TypeScriptMarch 2026RapidDev Engineering Team
TL;DR

Cursor can generate React Context providers with custom hooks when given clear type definitions and state management patterns. This tutorial shows how to prompt Cursor to create type-safe Context APIs with useReducer, memoized selectors, and provider composition, avoiding the common pitfalls of unnecessary re-renders and loosely typed context values.

Generating shared state logic with Cursor

React Context API combined with custom hooks provides a lightweight alternative to Redux for shared state. Cursor can scaffold entire Context systems quickly, but without guidance it generates untyped context, missing providers, and patterns that cause excessive re-renders. This tutorial establishes a pattern that produces performant, type-safe Context code every time.

Prerequisites

  • Cursor installed with a React + TypeScript project
  • React 18+ for useSyncExternalStore and modern Context features
  • Understanding of React Context and useReducer
  • Familiarity with Cursor Chat (Cmd+L) and Cmd+K

Step-by-step guide

1

Add Context generation rules to .cursor/rules

Create rules that enforce type-safe Context patterns and prevent common performance pitfalls. These rules ensure every Context Cursor generates follows best practices.

.cursor/rules/context.mdc
1---
2description: React Context generation conventions
3globs: "src/contexts/**/*.tsx,src/providers/**/*.tsx"
4alwaysApply: true
5---
6
7## Context Rules
8- ALWAYS create a typed context with createContext<Type | undefined>(undefined)
9- ALWAYS create a custom hook (useXxxContext) that throws if used outside provider
10- Split state and dispatch into separate contexts to prevent re-renders
11- Use useReducer for contexts with more than 2 state values
12- Memoize context values with useMemo to prevent reference changes
13- Place contexts in src/contexts/{name}Context.tsx
14- Export the provider component and the custom hook, NEVER the raw context

Expected result: Cursor generates typed, performant Context code following your conventions.

2

Generate a typed Context with custom hook

Ask Cursor to generate a complete Context system with provider, custom hook, and type definitions. Reference your rules and specify the state shape.

src/contexts/ThemeContext.tsx
1// Cursor Chat prompt (Cmd+L):
2// Generate a ThemeContext at src/contexts/ThemeContext.tsx
3// State: { theme: 'light' | 'dark', fontSize: number }
4// Actions: toggleTheme, setFontSize(size)
5// Use useReducer. Split state and dispatch contexts.
6// Export ThemeProvider and useTheme hook.
7
8import { createContext, useContext, useReducer, useMemo, ReactNode } from 'react';
9
10type Theme = 'light' | 'dark';
11interface ThemeState { theme: Theme; fontSize: number; }
12type ThemeAction =
13 | { type: 'TOGGLE_THEME' }
14 | { type: 'SET_FONT_SIZE'; payload: number };
15
16const ThemeStateContext = createContext<ThemeState | undefined>(undefined);
17const ThemeDispatchContext = createContext<React.Dispatch<ThemeAction> | undefined>(undefined);
18
19function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
20 switch (action.type) {
21 case 'TOGGLE_THEME':
22 return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
23 case 'SET_FONT_SIZE':
24 return { ...state, fontSize: action.payload };
25 default:
26 return state;
27 }
28}
29
30export function ThemeProvider({ children }: { children: ReactNode }) {
31 const [state, dispatch] = useReducer(themeReducer, { theme: 'light', fontSize: 16 });
32 const stateValue = useMemo(() => state, [state]);
33 return (
34 <ThemeStateContext.Provider value={stateValue}>
35 <ThemeDispatchContext.Provider value={dispatch}>
36 {children}
37 </ThemeDispatchContext.Provider>
38 </ThemeStateContext.Provider>
39 );
40}
41
42export function useTheme() {
43 const state = useContext(ThemeStateContext);
44 const dispatch = useContext(ThemeDispatchContext);
45 if (!state || !dispatch) throw new Error('useTheme must be used within ThemeProvider');
46 return { ...state, toggleTheme: () => dispatch({ type: 'TOGGLE_THEME' }), setFontSize: (size: number) => dispatch({ type: 'SET_FONT_SIZE', payload: size }) };
47}

Pro tip: Splitting state and dispatch into separate contexts prevents components that only dispatch from re-rendering when state changes.

Expected result: A type-safe Context with split providers, a reducer, and a custom hook with action helpers.

3

Generate a provider composition wrapper

When your app has multiple contexts, nesting providers creates deep component trees. Ask Cursor to generate a composition utility that flattens them.

src/providers/ComposeProviders.tsx
1// Cursor Chat prompt (Cmd+L):
2// Create a ComposeProviders utility component that accepts
3// an array of provider components and nests them automatically.
4// Use it to wrap ThemeProvider, AuthProvider, and ToastProvider.
5
6import { ReactNode, ComponentType } from 'react';
7
8type Provider = ComponentType<{ children: ReactNode }>;
9
10export function ComposeProviders({
11 providers,
12 children,
13}: {
14 providers: Provider[];
15 children: ReactNode;
16}) {
17 return providers.reduceRight(
18 (acc, Provider) => <Provider>{acc}</Provider>,
19 children
20 );
21}
22
23// Usage in App.tsx:
24// <ComposeProviders providers={[ThemeProvider, AuthProvider, ToastProvider]}>
25// <App />
26// </ComposeProviders>

Expected result: A utility that flattens nested providers into a clean, readable composition.

4

Generate selector hooks for granular subscriptions

For contexts with many state values, generate selector hooks that subscribe to specific fields. This prevents components from re-rendering when unrelated state changes.

src/contexts/ThemeContext.tsx
1// Cursor Chat prompt (Cmd+L):
2// @src/contexts/ThemeContext.tsx Create a useThemeSelector
3// hook that accepts a selector function and only re-renders
4// when the selected value changes. Use useRef and useSyncExternalStore
5// or a simple equality check pattern.
6
7import { useRef, useCallback } from 'react';
8
9export function useThemeSelector<T>(selector: (state: ThemeState) => T): T {
10 const state = useContext(ThemeStateContext);
11 if (!state) throw new Error('useThemeSelector must be used within ThemeProvider');
12 return selector(state);
13}
14
15// Usage:
16// const theme = useThemeSelector(s => s.theme);
17// Only re-renders when theme changes, not when fontSize changes

Expected result: A selector hook that enables granular subscriptions to context state.

5

Test the Context with Cursor-generated tests

Generate tests that verify the context provider, custom hook, and error boundary behavior. Ask Cursor to test that the hook throws outside the provider.

Cursor Chat prompt
1// Cursor Chat prompt (Cmd+L):
2// @src/contexts/ThemeContext.tsx Generate Vitest tests for
3// ThemeContext. Test: 1) Provider renders children,
4// 2) useTheme returns correct initial state,
5// 3) toggleTheme switches between light and dark,
6// 4) useTheme throws when used outside ThemeProvider.
7// Use @testing-library/react renderHook.

Expected result: Tests verifying provider rendering, state management, and error boundary behavior.

Complete working example

src/contexts/ThemeContext.tsx
1import {
2 createContext,
3 useContext,
4 useReducer,
5 useMemo,
6 useCallback,
7 type ReactNode,
8 type Dispatch,
9} from 'react';
10
11export type Theme = 'light' | 'dark';
12
13export interface ThemeState {
14 theme: Theme;
15 fontSize: number;
16 fontFamily: string;
17}
18
19export type ThemeAction =
20 | { type: 'TOGGLE_THEME' }
21 | { type: 'SET_FONT_SIZE'; payload: number }
22 | { type: 'SET_FONT_FAMILY'; payload: string }
23 | { type: 'RESET' };
24
25const initialState: ThemeState = {
26 theme: 'light',
27 fontSize: 16,
28 fontFamily: 'Inter',
29};
30
31function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
32 switch (action.type) {
33 case 'TOGGLE_THEME':
34 return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
35 case 'SET_FONT_SIZE':
36 return { ...state, fontSize: Math.max(12, Math.min(24, action.payload)) };
37 case 'SET_FONT_FAMILY':
38 return { ...state, fontFamily: action.payload };
39 case 'RESET':
40 return initialState;
41 default:
42 return state;
43 }
44}
45
46const StateCtx = createContext<ThemeState | undefined>(undefined);
47const DispatchCtx = createContext<Dispatch<ThemeAction> | undefined>(undefined);
48
49export function ThemeProvider({ children }: { children: ReactNode }) {
50 const [state, dispatch] = useReducer(themeReducer, initialState);
51 const memoState = useMemo(() => state, [state]);
52
53 return (
54 <StateCtx.Provider value={memoState}>
55 <DispatchCtx.Provider value={dispatch}>
56 {children}
57 </DispatchCtx.Provider>
58 </StateCtx.Provider>
59 );
60}
61
62export function useTheme() {
63 const state = useContext(StateCtx);
64 const dispatch = useContext(DispatchCtx);
65 if (!state || !dispatch) {
66 throw new Error('useTheme must be used within a ThemeProvider');
67 }
68
69 const toggleTheme = useCallback(() => dispatch({ type: 'TOGGLE_THEME' }), [dispatch]);
70 const setFontSize = useCallback((s: number) => dispatch({ type: 'SET_FONT_SIZE', payload: s }), [dispatch]);
71 const setFontFamily = useCallback((f: string) => dispatch({ type: 'SET_FONT_FAMILY', payload: f }), [dispatch]);
72 const reset = useCallback(() => dispatch({ type: 'RESET' }), [dispatch]);
73
74 return { ...state, toggleTheme, setFontSize, setFontFamily, reset };
75}

Common mistakes when generating shared state logic with Cursor

Why it's a problem: Creating a single context for both state and dispatch

How to avoid: Split into StateContext and DispatchContext. Add this pattern to your .cursorrules so Cursor does it automatically.

Why it's a problem: Forgetting to memoize the context value

How to avoid: Add 'Memoize context values with useMemo' to your rules. Cursor will wrap values in useMemo automatically.

Why it's a problem: Exporting the raw context instead of a custom hook

How to avoid: Export only the provider and custom hook. Add 'NEVER export the raw context' to .cursorrules.

Best practices

  • Split state and dispatch into separate contexts to minimize re-renders
  • Always create a custom hook that throws if used outside the provider
  • Memoize context values with useMemo to prevent unnecessary reference changes
  • Use useReducer for contexts with more than two state fields
  • Use useCallback for action dispatchers returned from custom hooks
  • Export only the provider and custom hook, never the raw context object
  • Use ComposeProviders utility to flatten deeply nested provider trees

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Create a React ThemeContext with TypeScript that manages theme (light/dark), fontSize, and fontFamily. Use useReducer for state management. Split into separate state and dispatch contexts to prevent unnecessary re-renders. Export a ThemeProvider component and a useTheme custom hook with toggleTheme, setFontSize, setFontFamily, and reset actions.

Cursor Prompt

In Cursor Chat (Cmd+L): @.cursor/rules/context.mdc Generate an AuthContext at src/contexts/AuthContext.tsx. State: { user: User | null, token: string | null, isLoading: boolean }. Actions: login(user, token), logout, setLoading. Use useReducer, split state/dispatch contexts, export AuthProvider and useAuth hook.

Frequently asked questions

When should I use Context vs Redux?

Use Context for UI state (theme, locale, auth status) that changes infrequently. Use Redux for complex application state with many subscribers, frequent updates, or middleware needs. Add this guidance to .cursorrules so Cursor suggests the right tool.

Can Cursor generate Context with Zustand instead?

Yes. Zustand is simpler than Context for many use cases. Tell Cursor: 'Use Zustand instead of React Context' in your prompt. Zustand automatically handles re-render optimization.

How do I test Context providers with Cursor?

Ask Cursor to generate tests using @testing-library/react's renderHook with a wrapper option. Reference the provider component and the custom hook in your prompt.

Should I use useReducer or useState in Context?

Use useReducer when the context has more than 2 state values or when state transitions are complex. Use useState for simple boolean or single-value contexts.

How do I persist Context state to localStorage?

Ask Cursor to add a useEffect that saves state to localStorage on changes and initializes from localStorage on mount. Specify 'Add localStorage persistence with a useEffect hook' in your prompt.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.