useRef — a mutable box that survives re-renders
useRef returns an object { current: value } that persists across renders but, unlike state, updating it does not trigger a re-render. Two main uses:
1. Accessing a DOM node directly
function TextInput() {
const inputRef = useRef(null);
useEffect(() => { inputRef.current.focus(); }, []);
return <input ref={inputRef} placeholder="Auto-focused" />;
}
2. Storing a mutable value that shouldn't cause a re-render
function Stopwatch() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
intervalRef.current = setInterval(() => setSeconds(s => s + 1), 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
useContext — avoiding prop drilling
Prop drilling is passing a prop through many layers of components that don't need it, just so a deeply nested child can use it. Context lets you skip the middle layers.
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />; // doesn't need to know about theme at all
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={`btn-${theme}`}>Save</button>;
}
Context is best for genuinely global/cross-cutting data: theme, authenticated user, locale. For frequently changing data or complex app state, a dedicated state manager (Zustand, Redux Toolkit, Jotai) usually scales better than Context alone.
useReducer — state logic as a reducer function
When state transitions are complex (multiple sub-values, or the next state depends on the action taken), useReducer keeps update logic in one predictable place, similar to Redux:
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { 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 (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
useReducer + useContext together is a common lightweight pattern for sharing state and actions across a component subtree without pulling in an external library.