Verified against Claude Code · 2026-07-13
Migrate a class component to hooks without silently dropping lifecycle behavior
A class-to-function migration prompt that maps each lifecycle method to its hook equivalent explicitly, and calls out the cases where the mapping isn't 1:1.
The prompt
Ready to copy — highlighted parts are example details you can swap.
<role>
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.
</role>
<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(); } ... }
</class_component>
<lifecycle_notes>
componentDidMount, componentDidUpdate, componentWillUnmount, shouldComponentUpdate
</lifecycle_notes>
<migration_map>
Map each lifecycle method explicitly, and flag any that don't have a clean 1:1 hook equivalent:
- constructor's this.state → one or more useState calls, split by what actually changes together, not one giant state object by default.
- componentDidMount + componentDidUpdate (when they run the same logic) → a single useEffect with the right dependency array. If they run genuinely different logic, that's a sign they need to become separate useEffect calls rather than one merged effect with awkward conditionals inside it.
- componentWillUnmount → the cleanup function returned from the relevant useEffect.
- shouldComponentUpdate → React.memo on the component, with a custom comparator only if the default shallow comparison is provably insufficient.
- getDerivedStateFromProps → usually a sign the value should be computed during render instead of stored in state at all — recommend removing the state entirely if that's true here.
- Instance variables that don't trigger a re-render (this.someRef, a mutable counter) → useRef.
- Error boundaries (componentDidCatch / static getDerivedStateFromError) → these have no hook equivalent — leave the error boundary as a class component and note it explicitly rather than inventing a hook-based substitute.
</migration_map>
<output_format>
1. The migrated function component.
2. A mapping table: old lifecycle method → 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 note) and why.
</output_format>Customize the highlighted detailsoptional — the prompt above already works
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, the effect now runs on mount only, or on every render, instead of on the specific prop change the class version handled — a silent behavior change that a naive migration won't flag. 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 full completion — componentDidCatch genuinely has no hook equivalent as of current React versions, and a migration that pretends otherwise ships something that silently stops catching errors. Naming getDerivedStateFromProps as usually a sign of unnecessary state pushes the model toward React's own recommended fix — compute the value during render — rather than a mechanical hook swap that would just relocate the same anti-pattern into a useEffect.
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 had no componentDidCatch, so a full function migration was possible.
Verified against
Claude Code Sonnet 4.6 · 2026-07-13
Cursor 2.1 · 2026-07-24
Changelog
- 2026-07-13 — 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

