Appearance
Common React Patterns
FreshComponent Patterns
Container / Presentational
Separate data-fetching logic from display logic:
jsx
// Container (handles data)
function UserListContainer() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers);
}, []);
return <UserList users={users} />;
}
// Presentational (handles display)
function UserList({ users }) {
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}Compound Components
Components that work together to form a complete UI:
jsx
function Select({ children, value, onChange }) {
return (
<SelectContext.Provider value={{ value, onChange }}>
<div className="select">{children}</div>
</SelectContext.Provider>
);
}
function Option({ value, children }) {
const { value: selected, onChange } = useContext(SelectContext);
return (
<div
className={value === selected ? 'selected' : ''}
onClick={() => onChange(value)}
>
{children}
</div>
);
}
// Usage
<Select value={color} onChange={setColor}>
<Option value="red">Red</Option>
<Option value="blue">Blue</Option>
</Select>Render Props
Pass a function as a prop to share logic:
jsx
function MouseTracker({ render }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return render(position);
}
// Usage
<MouseTracker render={({ x, y }) => <p>Mouse: {x}, {y}</p>} />TIP
Custom Hooks have largely replaced render props for sharing logic. Prefer hooks when possible.
Higher-Order Components (HOC)
A function that takes a component and returns a new component:
jsx
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const user = useAuth();
if (!user) return <LoginPage />;
return <WrappedComponent {...props} user={user} />;
};
}
const ProtectedDashboard = withAuth(Dashboard);TIP
Like render props, HOCs have been largely superseded by custom Hooks. Prefer hooks for new code.
State Patterns
Controlled vs Uncontrolled Components
jsx
// Controlled: React manages the value
function ControlledInput() {
const [value, setValue] = useState('');
return <input value={value} onChange={e => setValue(e.target.value)} />;
}
// Uncontrolled: DOM manages the value
function UncontrolledInput() {
const inputRef = useRef(null);
function handleSubmit() {
console.log(inputRef.current.value);
}
return <input defaultValue="" ref={inputRef} />;
}State Reducer Pattern
Give consumers control over state logic:
jsx
function useToggle({ reducer = toggleReducer } = {}) {
const [state, dispatch] = useReducer(reducer, { on: false });
const toggle = () => dispatch({ type: 'toggle' });
return { on: state.on, toggle };
}
function toggleReducer(state, action) {
switch (action.type) {
case 'toggle':
return { on: !state.on };
default:
return state;
}
}Data Fetching Patterns
useEffect + useState
jsx
function useData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}Avoid Fetching in useEffect for Production
For production apps, use a framework with built-in data fetching (Next.js, Remix) or a client-side caching library (React Query, SWR). Raw useEffect fetching misses caching, race conditions, and server rendering.
Performance Patterns
Debouncing State Updates
jsx
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const timeoutRef = useRef(null);
function handleChange(e) {
const value = e.target.value;
setQuery(value);
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => onSearch(value), 300);
}
return <input value={query} onChange={handleChange} />;
}Virtualized Lists
For long lists, only render visible items:
jsx
// Use react-window or @tanstack/react-virtual
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={35}
width={300}
>
{({ index, style }) => (
<div style={style}>{items[index]}</div>
)}
</FixedSizeList>
);
}Lazy Loading Components
jsx
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Mutating state directly | React won't detect changes | Create new objects/arrays |
| Derived state in useState | State gets out of sync | Compute during render |
| useEffect for events | Runs on every render, not just events | Use event handlers |
| Props in initial state | Initial state ignores prop updates | Use props directly or add a key |
| Index as key | Bugs with reordering/filtering | Use stable unique IDs |
| Deeply nested state | Hard to update immutably | Flatten/normalize state |