| name | react-state |
| description | React mental models |
Dan Abramov React Philosophy
Applying Dan Abramov's React mental models and "Just JavaScript" thinking when building React components, managing state, designing component APIs, or reviewing React code. Auto-triggers on .jsx/.tsx files, React component creation, hooks implementation, and state management decisions. Not for non-React JavaScript, not for build tooling, not for styling decisions.
Core Mental Models
React is "Just JavaScript"
Components are functions. Props are arguments. JSX is syntax sugar for function calls. State is variables that trigger re-renders. Effects are synchronization with external systems.
<Button onClick={handleClick}>Save</Button>
React.createElement(Button, { onClick: handleClick }, 'Save')
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
Apply when: Developer seems confused about React "magic" - ground them in JavaScript fundamentals.
UI as a Function of State
UI = f(state)
The component renders based on current state. Every render is a snapshot. Props and state during a render are frozen - they describe the UI at that moment in time.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
alert(`Count was: ${count}`);
}, 3000);
}
return <button onClick={handleClick}>{count}</button>;
}
Apply when: Debugging stale closure issues, explaining why async callbacks see "old" values.
Each Render Has Its Own Everything
Every render has its own props, state, event handlers, and effects. They're all closures over that render's values.
function Chat({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
Apply when: Explaining useEffect dependencies, fixing infinite loops, understanding cleanup.
Component Design Philosophy
Composition Over Configuration
Prefer composing components over adding props/flags.
<Dialog
title="Confirm"
showCloseButton
closeButtonPosition="top-right"
showFooter
footerAlignment="right"
primaryButtonText="OK"
secondaryButtonText="Cancel"
/>
<Dialog>
<Dialog.Header>
<Dialog.Title>Confirm</Dialog.Title>
<Dialog.CloseButton />
</Dialog.Header>
<Dialog.Body>Are you sure?</Dialog.Body>
<Dialog.Footer>
<Button variant="secondary" onClick={onCancel}>Cancel</Button>
<Button variant="primary" onClick={onConfirm}>OK</Button>
</Dialog.Footer>
</Dialog>
Apply when: Component has 5+ boolean props, props control layout/structure, different use cases need different DOM structure.
Inversion of Control
Let the parent decide what to render. Pass components/elements as props when the child shouldn't dictate structure.
<List items={users} />
<List>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</List>
<DataFetcher url="/api/users">
{({ data, loading, error }) => (
loading ? <Spinner /> : <UserList users={data} />
)}
</DataFetcher>
Apply when: Child component needs flexibility in what it renders, reusing logic but not UI.
Lift State Up (Only When Needed)
State lives where it's used. Lift only when siblings need to share.
function SearchPage() {
return (
<>
<SearchInput /> {/* Has its own query state */}
<FilterPanel /> {/* Has its own filter state */}
<Results /> {/* Fetches based on URL params */}
</>
);
}
function SearchPage() {
const [query, setQuery] = useState('');
return (
<>
<SearchInput value={query} onChange={setQuery} />
<Results query={query} /> {/* Now they're synchronized */}
</>
);
}
State Management
useState for Local State
Simple, isolated state that doesn't need to be shared.
function Toggle() {
const [isOn, setIsOn] = useState(false);
return <Switch checked={isOn} onChange={() => setIsOn(!isOn)} />;
}
useReducer for Complex State Logic
When state updates depend on previous state, or when you have multiple sub-values that update together.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return { count: 0 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
}
Context for "Global" State (Sparingly)
Context is for values that are truly global to a subtree: themes, locale, current user.
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
Hooks Philosophy
Custom Hooks Extract Logic, Not UI
Custom hooks are for reusing stateful logic, not for sharing UI.
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
function Header() {
const { width } = useWindowSize();
return width < 768 ? <MobileHeader /> : <DesktopHeader />;
}
useEffect is for Synchronization
Effects synchronize React with external systems. They're not lifecycle methods.
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
function Component({ items }) {
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);
const filteredItems = items.filter(i => i.active);
}
You Might Not Need an Effect
Many things that feel like effects are actually:
- Derived values: Just compute them during render
- Event responses: Handle in event handlers
- Initialization: Use lazy initializer in useState
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
const fullName = `${firstName} ${lastName}`;
useEffect(() => {
if (submitted) {
sendAnalytics('form_submitted');
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
sendAnalytics('form_submitted');
}
Anti-Patterns to Avoid
Premature Abstraction
Don't create abstractions until you've seen the pattern at least 3 times.
function useApi(url) { }
function useUserData(userId) {
}
Props Spreading Blindly
function Button(props) {
return <button {...props} />;
}
function Button({ onClick, children, disabled, className }) {
return (
<button
onClick={onClick}
disabled={disabled}
className={className}
>
{children}
</button>
);
}
Unnecessary Memoization
Only memoize when you've measured a performance problem.
const MemoizedComponent = React.memo(SimpleComponent);
const memoizedValue = useMemo(() => a + b, [a, b]);
const memoizedCallback = useCallback(() => doThing(), []);
Decision Framework
When to Split a Component
- It has multiple responsibilities - Header renders nav AND user menu AND search
- Part of it re-renders independently - Timer inside static card
- It's hard to name - "HeaderWithSearchAndUserMenuAndNotifications"
- You want to reuse part of it - The user avatar appears elsewhere
When to Keep Components Together
- They always change together - Form and its validation display
- One is meaningless without the other - Tab and TabPanel
- Splitting adds prop drilling - Without simplifying anything
When to Use Which State Management
| Scenario | Approach |
|---|
| Local UI state | useState |
| Complex state logic | useReducer |
| Shared between siblings | Lift state up |
| Deep prop drilling (infrequent changes) | Context |
| Frequent updates needed everywhere | External library (Zustand, Jotai) |
| Server state | React Query, SWR |
Code Review Checklist
When reviewing React code, verify:
Resources