React

Verified against Claude Code · 2026-07-25

Migrate a class component to hooks without silently dropping lifecycle behavior

A class-to-function migration that maps each lifecycle method to its hook equivalent explicitly, and names the cases where the mapping isn't 1:1 rather than papering over them.

Claude CodeCursorGitHub Copilot ChatChatGPT4 fillable variables

The prompt

Ready to copy — highlighted parts are example details you can swap.

You are migrating a class component to a function component with hooks. You preserve the exact same behavior — this is a mechanical migration, not an opportunity to also redesign the component or fix unrelated issues you notice along the way.

CLASS COMPONENT
class UserPanel extends React.Component { constructor(props) { super(props); this.state = { user: null, loading: true }; } componentDidMount() { this.fetchUser(); } componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) this.fetchUser(); } componentWillUnmount() { this.controller?.abort(); } }

LIFECYCLE METHODS IN USE
componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate

ERROR BOUNDARY LOGIC
No, this specific component has no error-boundary logic.

TARGET REACT VERSION
React 19

MIGRATION MAP
Map each lifecycle method explicitly, and flag any that do not have a clean 1:1 hook equivalent rather than inventing a workaround to claim full completion. The constructor's this.state becomes one or more useState calls, split by what actually changes together, not collapsed by default into one giant state object just because that mirrors the original this.state shape. componentDidMount and componentDidUpdate, when they run the exact same logic, become a single useEffect with the correct dependency array; if they run genuinely different logic in the class version, that is a sign they need to become two separate useEffect calls in the function version rather than one merged effect with awkward conditionals recreating the class's implicit branching inside it. componentWillUnmount becomes the cleanup function returned from the relevant useEffect. shouldComponentUpdate becomes React.memo on the component, with a custom comparator only if the default shallow-equality comparison is provably insufficient for this specific component's props. getDerivedStateFromProps is usually a sign the value should be computed during render instead of stored in state at all — if that is true here, recommend removing the state entirely rather than mechanically translating it into a hook. Instance variables that do not trigger a re-render, such as this.someRef or a mutable counter, become useRef. Error boundaries — componentDidCatch and static getDerivedStateFromError — have no hook equivalent as of the target React version; leave that logic as a class component and state that explicitly rather than inventing a hook-based substitute that would silently stop catching errors. static contextType or a Context.Consumer render-prop pattern used to read context becomes a direct useContext(SomeContext) call at the top of the function body — a strict simplification with no behavior difference, worth naming explicitly since a static class property is easy to miss when scanning a class component specifically for lifecycle methods. defaultProps defined as a static class property becomes default values in the function's own parameter destructuring — function Component({ size = 'medium' }) — rather than a separate defaultProps object kept alongside a function component, where recent React versions no longer give it any special meaning at all.

OUTPUT FORMAT
1. The migrated function component, as complete code.
2. A mapping table: old lifecycle method to new hook, with one line on any behavior difference, however small, introduced by the change.
3. Anything left as a class component per the error-boundary rule above, and exactly why.

Customize

Optional — swap in your own details for the highlighted parts above.

Why this works

The biggest real risk in this migration isn't syntax, it's timing: componentDidMount and componentDidUpdate get collapsed into one useEffect constantly, and if the dependency array doesn't exactly capture what previously triggered componentDidUpdate's own internal comparison, the effect now runs either on mount only or on every render instead of specifically on the one prop change the class version handled — a silent behavior change that a naive line-by-line migration will not catch because the code still compiles and still looks plausible. Requiring the model to name any lifecycle method without a clean hook equivalent, especially the error-boundary carve-out, stops it from inventing a broken workaround just to claim a complete function-component migration — componentDidCatch genuinely has no hook equivalent in current React, and a migration that pretends otherwise ships something that silently stops catching render errors the moment it's merged, which is exactly the kind of regression that only surfaces in production when it's too late to trace back to this specific change. Naming getDerivedStateFromProps as usually a sign of unnecessary state pushes the model toward React's own recommended fix — compute the value during render instead of storing a derived copy — rather than a purely mechanical hook swap that would just relocate the same anti-pattern into a useEffect and call the migration done without actually improving anything. The static contextType callout matters because it is genuinely easy to miss during a lifecycle-focused migration: a reviewer scanning for componentDidMount and componentDidUpdate can walk right past a static contextType assignment sitting quietly near the top of the class, and if it is missed, the migrated function component silently loses its subscription to that context and starts rendering with undefined where a real value used to be — a regression a lifecycle-mapping table alone would never surface unless context reads were checked as their own category. The defaultProps translation is worth stating explicitly because it is not merely a stylistic mismatch: recent React versions do not honor a defaultProps static property on function components the way they did on classes, so a mechanical copy-paste of the same pattern does not just look different, it silently stops working, and default values have to move into the destructured parameters themselves to have any effect at all.

What you get back

Old lifecycle | New hook | Behavior note constructor's this.state | useState(null) for user, useState(true) for loading | split into two calls since loading and user don't always change together componentDidMount + componentDidUpdate (prevProps.userId check) | useEffect(() => { /* fetch */ }, [userId]) | identical trigger condition, now expressed as a dependency instead of a manual prop comparison componentWillUnmount (abort controller) | cleanup function returned from the same useEffect | identical behavior shouldComponentUpdate | React.memo(UserPanel) | default shallow comparison covers the same fields the original method checked Left as a class component: none — this component has no componentDidCatch, so a full function migration was possible without any carve-out.

Verified against

Claude Code Sonnet 4.6 · 2026-07-25

Cursor Cursor 2.1 · 2026-07-24

Changelog

  • 2026-07-25 Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All React prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY