How does React handle the component lifecycle in functional components?
Function components don't have lifecycle methods — they re-run top-to-bottom on every render. Hooks emulate the lifecycle: useState/useRef = instance fields; useEffect with [] = componentDidMount + componentWillUnmount; useEffect with deps = componentDidUpdate; useLayoutEffect = synchronous post-commit work; the function body itself = render. Reasoning shifts from 'phases' to 'this state at this moment'.
Class lifecycle methods don't exist for function components — hooks replace them.
Mental model shift
A function component is a render function. Every render produces a snapshot of UI for a given state. Hooks bridge that snapshot to React's machinery.
Mapping
| Class method | Function equivalent |
|---|---|
| constructor | useState initializer; useRef for instance vars |
| componentDidMount | useEffect(() => {...}, []) |
| componentDidUpdate | useEffect(() => {...}, [deps]) |
| componentWillUnmount | cleanup returned from useEffect |
| shouldComponentUpdate | React.memo + custom compare |
| getDerivedStateFromProps | compute in render, or use a key to reset |
| getSnapshotBeforeUpdate | useLayoutEffect |
| componentDidCatch | error boundaries (still class-based, but consumable from FCs) |
Side-by-side
// Class
class Clock extends Component {
state = { time: new Date() };
componentDidMount() { this.id = setInterval(this.tick, 1000); }
componentWillUnmount() { clearInterval(this.id); }
tick = () => this.setState({ time: new Date() });
render() { return <div>{this.state.time.toLocaleTimeString()}</div>; }
}
// Function
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(id);
}, []);
return <div>{time.toLocaleTimeString()}</div>;
}Phase order in a function render
- Body executes top-to-bottom, hooks register their slots.
- JSX is returned.
- React reconciles + commits.
useLayoutEffectfires synchronously (blocks paint).- Browser paints.
useEffectfires async.
On the next render, cleanups from the previous render run BEFORE the new effects.
Things that don't map 1:1
- componentWillReceiveProps / getDerivedStateFromProps: usually a smell. Prefer deriving in render or using a
keyto reset state. - forceUpdate: no direct equivalent — use a
useReducerincrement or a state flip. - Error boundaries: still require a class (no FC API yet). Wrap your FC tree.
Refs for instance variables
Anything you would have stored on this becomes a useRef:
const intervalRef = useRef<number | null>(null);Mutating .current doesn't trigger a re-render — perfect for handles, IDs, accumulators.
Hidden lifecycle: each render is its own snapshot
State and props are captured at render time. A closure inside setTimeout sees the values from when it was created, not the latest values. This is the root cause of most 'stale closure' bugs — and why functional updaters (setX(prev => ...)) and refs exist as escape hatches.
Follow-up questions
- •What does StrictMode's double-mount reveal about effect lifecycle?
- •When would you reach for useLayoutEffect over useEffect?
- •How do you reset state when a prop changes — without an effect?
Common mistakes
- •Trying to replicate componentWillReceiveProps with useEffect — usually leads to bugs and infinite loops.
- •Storing non-reactive 'instance' values in useState — useRef is the right tool.
- •Reading state in a setTimeout that was set up on mount, expecting the latest value — stale closure.
Performance considerations
- •Effects are async (run after paint), so they don't block the browser. `useLayoutEffect` blocks paint — use sparingly. Class lifecycle methods had the same cost as their hook equivalents; the difference is ergonomic, not performance.
Edge cases
- •useEffect with no deps runs after every render — almost always a mistake.
- •Effects from unmounted components don't run, but pending fetches still resolve — guard with AbortController.
- •StrictMode dev double-mount: setup → cleanup → setup runs once on mount in development.
Real-world examples
- •Every modern React component is a function component. Class components survive mostly in older codebases and for error boundaries. New code: function + hooks, every time.