React Component Lifecycle: From Class Methods to Hooks
Every React component has a lifecycle — it mounts, updates, and unmounts. Understanding this is foundational for any React interview.
The Three Phases
Mounting — component is created and inserted into the DOM for the first time.
Updating — component re-renders because props or state changed.
Unmounting — component is removed from the DOM.
Class Component Lifecycle Methods
class Timer extends React.Component {
// 1. MOUNTING
constructor(props) {
super(props)
this.state = { seconds: 0 }
// initialize state, bind methods — no side effects here
}
static getDerivedStateFromProps(props, state) { // runs before every render — return state update or null // rarely needed; usually a sign of over-engineering return null }
componentDidMount() { // fires ONCE after first DOM insertion // safe for: data fetching, subscriptions, DOM manipulation this.interval = setInterval(() => this.setState(s => ({ seconds: s.seconds + 1 })), 1000) }
// 2. UPDATING shouldComponentUpdate(nextProps, nextState) { // optimization: return false to skip re-render // React.PureComponent does shallow compare automatically return true }
componentDidUpdate(prevProps, prevState) { // fires after every update (not first render) if (prevProps.userId !== this.props.userId) { this.fetchUser(this.props.userId) // guard required — no infinite loop } }
// 3. UNMOUNTING componentWillUnmount() { // cleanup before removal — timers, subscriptions, listeners clearInterval(this.interval) }
render() { return <div>{this.state.seconds}s</div> } }
Hooks Equivalents
function Timer() {
const [seconds, setSeconds] = useState(0)
// componentDidMount — empty deps array = runs once after mount useEffect(() => { const id = setInterval(() => setSeconds(s => s + 1), 1000)
// componentWillUnmount — cleanup function return () => clearInterval(id) }, [])
// componentDidUpdate — non-empty deps array useEffect(() => { document.title = 'Timer: ' + seconds }, [seconds])
return <div>{seconds}s</div> }
Lifecycle → Hook Mapping Table
| Class Method | Hook Equivalent | Notes | |---|---|---| | constructor | useState initializer | Lazy init: useState(() => fn()) | | componentDidMount | useEffect(fn, []) | Empty array = once after mount | | componentDidUpdate | useEffect(fn, [deps]) | Runs on mount too — add guard if needed | | componentWillUnmount | useEffect(() => cleanup, []) | Cleanup fn = unmount | | getDerivedStateFromProps | useState + useMemo | Compute derived state during render | | shouldComponentUpdate | React.memo | Wrap component for shallow prop compare | | getSnapshotBeforeUpdate | useRef | Store value before paint in a ref | | componentDidCatch | No hooks equivalent | Error boundaries are class-only |
Error Boundaries (Class Only)
Error boundaries catch errors in their child tree and display fallback UI. There is no hooks equivalent — they must be class components.
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError(error) { // render phase — must be pure, update state only return { hasError: true } }
componentDidCatch(error, info) { // commit phase — safe for logging, analytics Sentry.captureException(error, { extra: info }) }
render() { if (this.state.hasError) return <h2>Something went wrong.</h2> return this.props.children } }
The UNSAFE_ Methods (Deprecated)
Three lifecycle methods were renamed with UNSAFE_ prefix in React 16.3:
UNSAFE_componentWillMount
UNSAFE_componentWillReceiveProps
UNSAFE_componentWillUpdate
They were deprecated because React's concurrent rendering can pause and restart renders — these methods could fire multiple times before a commit, making them unsafe for side effects or subscriptions.
Interview answer: "These methods caused bugs with async rendering. The UNSAFE_ prefix signals they'll be removed. componentWillMount → useEffect(fn, []). componentWillReceiveProps → getDerivedStateFromProps or useEffect with deps."
The useLayoutEffect Edge Case
useLayoutEffect fires synchronously after all DOM mutations but before the browser paints. Use it only for DOM measurements that must happen before the user sees the result:
// Measure a DOM node before paint to avoid flicker
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect()
setPosition(rect)
}, [])
Default to useEffect. Switch to useLayoutEffect only if you see visual flicker.
Practice lifecycle questions at [JSPrep Pro](/auth).