react-guidelines
React Guidelines
This document outlines best practices for building robust, maintainable, and modern React applications with TypeScript.
1. General Principles
- Component Composition: Build small, focused components and compose them to build complex UIs.
- Unidirectional Data Flow: Data flows down (props), actions flow up (callbacks).
- Immutability: Treat state as immutable. Use functional updates and immutable patterns.
- Colocation: Keep related logic, styles, and tests close to the component.
2. TypeScript Integration
-
Strict Typing: Always use
strict: trueintsconfig.json. avoidinganyensures type safety. -
Props Interfaces: Define explicit interfaces for component props.
interface ButtonProps { label: string; onClick: () => void; variant?: 'primary' | 'secondary'; } -
Discriminated Unions: Use discriminated unions for state that can be in distinct modes (e.g., handling loading/success/error states).
type State = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: User } | { status: 'error'; error: Error }; -
Event Types: Use React's built-in event types (e.g.,
React.ChangeEvent<HTMLInputElement>,React.FormEvent).
3. State Management Best Practices
Derived State (Crucial)
-
Avoid Redundant State: Do not store state that can be calculated from existing props or other state.
-
Calculate on Render: Compute values directly in the component body.
-
Bad:
const [filteredList, setFilteredList] = useState([]); // This is an anti-pattern: syncing state useEffect(() => { setFilteredList(items.filter(i => i.active)); }, [items]); -
Good:
// Calculated during render. Always fresh, no sync bugs. const filteredList = items.filter(i => i.active);
-
-
Memoization: Use
useMemoonly if the calculation is computationally expensive.
useState vs useReducer
- Use
useStatefor simple, independent values. - Use
useReducerfor complex state logic, or when the next state depends on the previous one in complex ways.
4. useEffect Usage and Pitfalls
- Synchronization, Not Data Flow:
useEffectis for synchronizing with external systems (APIs, DOM, subscriptions). It is not for transforming data or "watching" props to update state. - Fetching Data: When fetching data, handle race conditions (e.g., ignore results if the component unmounts or the query changes).
- Strict Dependencies: Always include all variables used in the effect in the dependency array.
- Cleanup Functions: Always return a cleanup function for effects that create subscriptions or event listeners.
5. Component Patterns
- Custom Hooks: Extract logic into custom hooks (
useUser,useWindowSize). This keeps components focused on UI. - Container/Presentational: While strictly rigidly separating them is less common now, separating logically complex data-fetching components from pure UI components is still good practice.
- Fragments: Use
<>(Fragments) to avoid unnecessary DOM wrapper nodes.
6. Performance
- Stable Identity: Wrap functions passed as props in
useCallbackonly if the child component is wrapped inReact.memoor if the function is a dependency of minimal effect. - Lists: Always use a unique, stable
keyfor list items. Do not use array index. - Lazy Loading: Use
React.lazyandSuspensefor route-level code splitting.