skip to content

Your team owns a React codebase with hundreds of class components. How would you decide whether and how to migrate them to function components with hooks?

level: principalimportance: should knowfreq 28%

answer

  1. interop is free, so incremental is free
  2. migrate on contact, not on principle
  3. tie it to a capability you want
  4. one thing still needs a class
  5. budget for the non-mechanical translations

basics

~20 s

Classes still work in React 19, so migration is an investment decision, not a repair. Because classes and function components interoperate freely, migrate opportunistically — where a component blocks hook-only APIs, sits on a hot path, or is being changed anyway — never as a standalone rewrite.

solid answer

~50 s

I would start from the fact that class components are not broken: they still render in React 19, and they compose with function components without any bridge, so there is no forced big-bang. That makes it a question of what the classes cost. Concretely they cost access to everything hooks-only — `use`, Actions and the newer form primitives — and they are invisible to the React Compiler, which only auto-memoizes function components. So I would migrate on contact: any component being changed for product reasons gets converted as part of that work, plus a deliberate list of components that block a capability we want. I would leave alone the ones that are stable, untouched and cheap, and keep the pieces that genuinely require a class — error boundaries still do. The two risks worth budgeting for are the translations that are not mechanical, `getDerivedStateFromProps` and `setState` callbacks especially, and test churn; I would require each migration diff to be behaviour-preserving with tests already in place before the conversion.

go deeper

for a junior

Know that class and function components work together in the same app, so a codebase can hold both indefinitely and you do not have to convert a component just to work near it.

for a middle

Be able to name which conversions are mechanical and which are not — componentDidMount to an effect is routine, while getDerivedStateFromProps and setState callbacks need real thought.

for a senior

Argue the cost side with specifics: hooks-only APIs a class cannot reach, the React Compiler skipping classes entirely, and the migration bugs that cluster around a handful of lifecycle translations.

for a principal

Own the programme shape: no new classes, migrate on contact, a short explicit list tied to capability or measurement, behaviour-preserving diffs with tests first, and the confidence to say most of the codebase should be left alone.

## Frame it as investment, not repair The wrong opening is "classes are legacy, we should rewrite them". Class components render fine in React 19, and mixing them with function components requires no adapter — a class can render a function component and vice versa, and both see the same context and the same reconciliation. That means the migration can be arbitrarily incremental, which in turn means a big-bang rewrite is the one option with no justification. So the real question is what staying on classes costs, component by component. ## What classes actually cost **Capability.** Hooks are function-only. A class cannot call `use`, cannot use the Actions-era form primitives, and cannot consume any of the increasing number of libraries that ship hooks as their only API. When a class blocks a feature, that class has a business reason to move. **Automatic optimization.** The React Compiler inserts memoization at build time for function components that follow the Rules of React. Classes get none of it. For a component on a hot render path, that is a concrete, measurable difference, not a stylistic one. **Comprehension.** Lifecycle-shaped code splits one concern across three methods and merges unrelated concerns into one method. Effects group by concern instead. That is a real maintenance cost, but it is the softest of the three and should not be the headline argument. **Recruiting and review.** Newer engineers read hooks fluently and lifecycle code less so, which shows up as slower reviews and more subtle bugs in the class parts of the codebase. ## The policy I would set 1. **No new classes.** Enforce it in review. This alone stops the debt growing. 2. **Migrate on contact.** A component being modified for product reasons is converted in that change, while someone already has the behaviour paged in and there is a reason to test it. 3. **A short deliberate list.** Components that block a capability, sit on a measured hot path, or use the render-phase `UNSAFE_` lifecycles get scheduled explicitly with an owner. That list should be finite and visible. 4. **Leave the rest.** A stable component nobody has touched in two years is not costing anything; converting it spends review time and adds risk for no return. ## What stays a class Error boundaries still require a class — the boundary methods have no hook equivalent — so either keep a small hand-written boundary class or wrap the well-known library that provides one. This is a narrow, contained exception, and pretending otherwise in an interview is a tell. ## Budget for the hard translations Most conversions are mechanical. A few are not, and these are where migrations introduce bugs: - `getDerivedStateFromProps` — usually indicates prop-to-state duplication; the right conversion often deletes the state rather than reproducing it. - `setState`'s second-argument callback — has no direct equivalent; needs to become an effect keyed on the value, or an updater function, depending on which of its two uses applies. - `componentDidUpdate` with previous-props comparisons — becomes a dependency array, and the naive port fires an extra time on mount. - Long-lived instance fields — must be moved deliberately to refs; treating them as ordinary variables in a function body loses them every render. ```jsx // Mechanical: this converts almost by hand. componentDidMount() { document.title = this.props.title; } componentDidUpdate() { document.title = this.props.title; } // → useEffect(() => { document.title = title; }, [title]); ``` ## Make it verifiable The rule I would insist on is that a migration diff changes no behaviour and touches no product logic, and that the component has tests before the conversion rather than after. Mixing a rewrite with a feature change is how a migration acquires a reputation for causing outages, and one visible incident will stop the programme regardless of its merits. ## The judgment being tested The interviewer is checking whether you can say "not all of it, and not now" with reasons. A strong answer names the interop property that makes incrementalism free, ties migration to a capability or a measurement rather than to taste, names the one thing that still needs a class, and shows awareness that a few specific lifecycle translations are where the bugs come from.

  • Are class components deprecated in React 19?
    No. They still render, and there is no removal announced. What has happened is that they stopped receiving new capabilities: hooks, the Actions and form primitives, and the React Compiler's automatic memoization are all function-component territory. The accurate framing is stagnation rather than deprecation — nothing breaks, but the gap in what you can build widens with each release.
  • Which components would you refuse to migrate, and why?
    Stable ones nobody is touching, where conversion buys nothing and spends review time and risk. Error boundaries, which still require a class. And anything scheduled for deletion or replacement — converting code on its way out is pure waste. The default answer to 'should we migrate this?' for an untouched, working component is no.
  • How would you keep a long-running migration from stalling halfway?
    Attach it to work that is happening anyway rather than funding it as a project, so it cannot be deprioritized as a unit. Block new classes in review so the denominator stops growing. Publish the remaining count so progress is visible. And keep the explicitly scheduled list short and owned, so it looks like a finite task rather than an open-ended one.
  • Does mixing class and function components in one tree cause any problems?
    No — they interoperate transparently. A class can render function children and vice versa, both read the same context, and reconciliation treats them identically. The only real constraints are that a class cannot call hooks, and that refs to class instances behave differently from refs to DOM nodes. That transparency is exactly what makes an incremental migration safe.

saying these in an interview costs you the question

  • Classes are deprecated, so everything must be rewritten now
  • Convert the whole codebase in one sweep so the style is consistent
  • Migrate and refactor product logic in the same pull request
  • Function components are always faster than class components
  • Nothing needs a class any more, error boundaries included

context