|
| 1 | +--- |
| 2 | +description: |
| 3 | +globs: |
| 4 | +alwaysApply: true |
| 5 | +--- |
| 6 | +# React Patterns and Best Practices |
| 7 | + |
| 8 | +## Core Philosophy |
| 9 | + |
| 10 | +- **UIs are thin wrappers over data** - avoid using local state (like useState) unless absolutely necessary and it's independent of business logic |
| 11 | +- Even when local state seems needed, consider if you can flatten the UI state into a basic calculation |
| 12 | +- useState is only necessary if it's truly reactive and cannot be derived |
| 13 | + |
| 14 | +## State Management |
| 15 | + |
| 16 | +- **Choose state machines over multiple useStates** - multiple useState calls make code harder to reason about |
| 17 | +- Prefer a single state object with reducers for complex state logic |
| 18 | +- Co-locate related state rather than spreading it across multiple useState calls |
| 19 | + |
| 20 | +## Component Architecture |
| 21 | + |
| 22 | +- **Create new component abstractions when nesting conditional logic** |
| 23 | +- Move complex logic to new components rather than deeply nested conditionals |
| 24 | +- Use ternaries only for small, easily readable logic |
| 25 | +- Avoid top-level if/else statements in JSX - extract to components instead |
| 26 | + |
| 27 | +## Side Effects and Dependencies |
| 28 | + |
| 29 | +- **Avoid putting dependent logic in useEffects** - it causes misdirection about what the logic is doing |
| 30 | +- Choose to explicitly define logic rather than depend on implicit reactive behavior |
| 31 | +- When useEffect is necessary, be explicit about dependencies and cleanup |
| 32 | +- Prefer derived state and event handlers over effect-driven logic |
| 33 | + |
| 34 | +## Timing and Async Patterns |
| 35 | + |
| 36 | +- **setTimeouts are flaky and usually a hack** - always provide a comment explaining why setTimeout is needed |
| 37 | +- Consider alternatives like: |
| 38 | + - Proper loading states |
| 39 | + - Suspense boundaries |
| 40 | + - Event-driven patterns |
| 41 | + - State machines with delayed transitions |
| 42 | + - requestAnimateFrame and queuMicrotask |
| 43 | + |
| 44 | +## Code Quality Impact |
| 45 | + |
| 46 | +These patterns prevent subtle bugs that pile up into major issues. While code may "work" without following these guidelines, violations often lead to: |
| 47 | +- Hard-to-debug timing issues |
| 48 | +- Unexpected re-renders |
| 49 | +- State synchronization problems |
| 50 | +- Complex refactoring requirements |
| 51 | + |
| 52 | +## Examples |
| 53 | + |
| 54 | +### ❌ Avoid: Multiple useState |
| 55 | +```tsx |
| 56 | +const [loading, setLoading] = useState(false); |
| 57 | +const [error, setError] = useState(null); |
| 58 | +const [data, setData] = useState(null); |
| 59 | +``` |
| 60 | + |
| 61 | +### ✅ Prefer: State machine |
| 62 | +```tsx |
| 63 | +function useLazyRef<T>(fn: () => T) { |
| 64 | + const ref = React.useRef<T | null>(null); |
| 65 | + |
| 66 | + if (ref.current === null) { |
| 67 | + ref.current = fn(); |
| 68 | + } |
| 69 | + |
| 70 | + return ref as React.RefObject<T>; |
| 71 | +} |
| 72 | + |
| 73 | +interface Store<T> { |
| 74 | + subscribe: (callback: () => void) => () => void |
| 75 | + getState: () => T |
| 76 | + setState: <K extends keyof T>(key: K, value: T[K]) => void |
| 77 | + notify: () => void |
| 78 | +} |
| 79 | + |
| 80 | +function createStore<T>( |
| 81 | + listenersRef: React.RefObject<Set<() => void>>, |
| 82 | + stateRef: React.RefObject<T>, |
| 83 | + onValueChange?: Partial<{ |
| 84 | + [K in keyof T]: (value: T[K], store: Store<T>) => void |
| 85 | + }> |
| 86 | +): Store<T> { |
| 87 | + const store: Store<T> = { |
| 88 | + subscribe: (cb) => { |
| 89 | + listenersRef.current.add(cb); |
| 90 | + return () => listenersRef.current.delete(cb); |
| 91 | + }, |
| 92 | + getState: () => stateRef.current, |
| 93 | + setState: (key, value) => { |
| 94 | + if (Object.is(stateRef.current[key], value)) return; |
| 95 | + stateRef.current[key] = value; |
| 96 | + onValueChange?.[key]?.(value, store); |
| 97 | + store.notify(); |
| 98 | + }, |
| 99 | + notify: () => { |
| 100 | + for (const cb of listenersRef.current) { |
| 101 | + cb(); |
| 102 | + } |
| 103 | + }, |
| 104 | + }; |
| 105 | + |
| 106 | + return store; |
| 107 | +} |
| 108 | + |
| 109 | +function useStoreSelector<T, U>( |
| 110 | + store: Store<T>, |
| 111 | + selector: (state: T) => U |
| 112 | +): U { |
| 113 | + const getSnapshot = React.useCallback( |
| 114 | + () => selector(store.snapshot()), |
| 115 | + [store, selector] |
| 116 | + ); |
| 117 | + |
| 118 | + return React.useSyncExternalStore( |
| 119 | + store.subscribe, |
| 120 | + getSnapshot, |
| 121 | + getSnapshot |
| 122 | + ); |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +### ❌ Avoid: Complex conditionals in JSX |
| 127 | +```tsx |
| 128 | +return ( |
| 129 | + <div> |
| 130 | + {user ? ( |
| 131 | + user.isAdmin ? ( |
| 132 | + <AdminPanel /> |
| 133 | + ) : user.isPremium ? ( |
| 134 | + <PremiumDashboard /> |
| 135 | + ) : ( |
| 136 | + <BasicDashboard /> |
| 137 | + ) |
| 138 | + ) : ( |
| 139 | + <LoginForm /> |
| 140 | + )} |
| 141 | + </div> |
| 142 | +); |
| 143 | +``` |
| 144 | + |
| 145 | +### ✅ Prefer: Component abstraction |
| 146 | +```tsx |
| 147 | +function UserDashboard({ user }) { |
| 148 | + if (!user) return <LoginForm />; |
| 149 | + if (user.isAdmin) return <AdminPanel />; |
| 150 | + if (user.isPremium) return <PremiumDashboard />; |
| 151 | + return <BasicDashboard />; |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### ❌ Avoid: Effect-driven logic |
| 156 | +```tsx |
| 157 | +useEffect(() => { |
| 158 | + if (user && user.preferences) { |
| 159 | + setTheme(user.preferences.theme); |
| 160 | + } |
| 161 | +}, [user]); |
| 162 | +``` |
| 163 | + |
| 164 | +### ✅ Prefer: Derived values |
| 165 | +```tsx |
| 166 | +const theme = user?.preferences?.theme ?? 'default'; |
| 167 | +``` |
| 168 | + |
0 commit comments