React Hooks in Class Components: What Works Instead

You cannot use React hooks inside a class component. Hooks only work in function components and in custom hooks, and React throws an error if you call one from a class. There are three practical ways round it: wrap the class in a small function component that calls the hook and passes the values down as props, use a render-prop component for the same job, or convert the class to a function component, which is usually the right answer. This guide shows the real error, all three approaches running, and a lifecycle-to-hooks map for the conversion.

Every example was built and run with React 19.3.0 in a Vite project, and the screenshots come from the browser. Reference: Rules of Hooks on react.dev.

Why hooks do not work in class components

Hooks rely on React keeping a list of state slots for the component that is currently rendering, in the order the hooks are called. A class component does not have that list: its state lives on the instance, in this.state. So when a hook is called from a class render, React cannot find a matching dispatcher and stops. This is the message it gives, caught and printed on the page:

import { Component, useState } from "react";

class Counter extends Component {
  render() {
    let message = "no error";
    try {
      const [count] = useState(0);          // a hook inside a class: not allowed
      message = `count is ${count}`;
    } catch (error) {
      message = `${error.name}: ${error.message}`;
      console.log("React said:", error.message);
    }

    return (
      <div style={{ fontFamily: "system-ui", padding: 16 }}>
        <h3 style={{ margin: "0 0 8px" }}>Calling useState() inside a class</h3>
        <pre style={{ background: "#fff4f4", border: "1px solid #f3c2c2", borderRadius: 6,
                      padding: 12, whiteSpace: "pre-wrap", color: "#8a1f1f" }}>{message}</pre>
      </div>
    );
  }
}

export default function App() {
  return <Counter />;
}
A browser showing the React error message produced by calling useState inside a class component render method
The class called useState() in render(). This is React’s own message.

You will see the same error from three other mistakes, which is worth knowing because the text does not tell you which one you hit: calling a hook inside a condition or a loop, having two copies of React in node_modules, and calling a hook from an event handler rather than the component body.

The fix most teams use: a wrapper component

The class stays exactly as it is and receives what it needs through props. A tiny function component around it calls the hooks. This is what withRouter and similar helpers have always done, and it is the least invasive change you can make to a large class:

import { Component, useState } from "react";

// 1. the class stays a class, and receives everything it needs as props
class OrderPanel extends Component {
  render() {
    const { count, increment, city } = this.props;
    return (
      <div style={{ fontFamily: "system-ui", padding: 16 }}>
        <h3 style={{ margin: "0 0 8px" }}>Orders from {city}</h3>
        <p id="count">Selected: {count}</p>
        <button id="add" onClick={increment}
          style={{ padding: "6px 12px", borderRadius: 6, border: "1px solid #0b6bcb",
                   background: "#0b6bcb", color: "#fff", cursor: "pointer" }}>
          Add an order
        </button>
      </div>
    );
  }
}

// 2. a function component calls the hooks and passes the values down
function withCounter(ClassComponent) {
  return function Wrapper(props) {
    const [count, setCount] = useState(0);
    const increment = () => setCount(current => current + 1);
    return <ClassComponent {...props} count={count} increment={increment} />;
  };
}

const OrderPanelWithCounter = withCounter(OrderPanel);

export default function App() {
  return <OrderPanelWithCounter city="Austin" />;
}
A class component receiving a counter value and an increment function as props from a hook wrapper, showing three orders selected after three clicks
The button was clicked three times: the hook holds the state, the class just renders it.

Pass callbacks down as well as values, as increment is here, so the class can update the state it does not own. If several classes need the same data, put the hooks in one wrapper and reuse it, in the same way you would pass a function to a child component.

The same idea as a render prop

If you would rather not wrap the class, invert it: a function component calls the hook and hands the value to a function you give it as children. This suits one-off cases, such as a single class that needs the window width:

import { Component, useState, useEffect } from "react";

// the hook lives in a function component that renders whatever you give it
function WindowWidth({ children }) {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const onResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  return children(width);
}

class Layout extends Component {
  render() {
    return (
      <WindowWidth>
        {(width) => (
          <p style={{ fontFamily: "system-ui", padding: 16 }}>
            The window is {width}px wide, so this is the {width < 768 ? "mobile" : "desktop"} layout.
          </p>
        )}
      </WindowWidth>
    );
  }
}

export default function App() {
  console.log("rendered at", window.innerWidth, "px");
  return <Layout />;
}

Output:

rendered at 900 px

Both approaches are the same trick in different clothing. The hook always runs inside a function component; the class only consumes the result.

Converting the class to a function component

This is the real fix. The two components below do the same job: a counter that ticks on a timer, logs when it passes two ticks, and cleans up when it is removed. The class uses three lifecycle methods; the function uses two effects. Both were mounted together and then unmounted with a button, and this is what each one logged:

import { Component, useState, useEffect } from "react";

// BEFORE: the class version
class ClassClock extends Component {
  constructor(props) {
    super(props);
    this.state = { ticks: 0 };
  }

  componentDidMount() {
    console.log("class: mounted");
    this.timer = setInterval(() => this.setState(s => ({ ticks: s.ticks + 1 })), 100);
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.ticks !== this.state.ticks && this.state.ticks === 2) {
      console.log("class: reached", this.state.ticks, "ticks");
    }
  }

  componentWillUnmount() {
    console.log("class: cleaned up");
    clearInterval(this.timer);
  }

  render() {
    return <p>class ticks: {this.state.ticks}</p>;
  }
}

// AFTER: the same component with hooks
function HookClock() {
  const [ticks, setTicks] = useState(0);

  useEffect(() => {                      // componentDidMount + componentWillUnmount
    console.log("hooks: mounted");
    const timer = setInterval(() => setTicks(t => t + 1), 100);
    return () => {
      console.log("hooks: cleaned up");
      clearInterval(timer);
    };
  }, []);

  useEffect(() => {                      // componentDidUpdate, but only for ticks
    if (ticks === 2) console.log("hooks: reached", ticks, "ticks");
  }, [ticks]);

  return <p>hook ticks: {ticks}</p>;
}

export default function App() {
  const [show, setShow] = useState(true);

  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <button id="toggle" onClick={() => setShow(s => !s)}
        style={{ padding: "6px 12px", marginBottom: 8, borderRadius: 6,
                 border: "1px solid #0b6bcb", background: "#fff", color: "#0b6bcb", cursor: "pointer" }}>
        unmount both
      </button>
      {show && (
        <>
          <ClassClock />
          <HookClock />
        </>
      )}
    </div>
  );
}

Console output from mounting both, then unmounting them:

class: mounted
hooks: mounted
class: reached 2 ticks
hooks: reached 2 ticks
class: cleaned up
hooks: cleaned up

Notice the order in the output: with hooks the cleanup runs in the same effect that created the timer, so the setup and the teardown sit next to each other instead of in methods a hundred lines apart.

Class lifecycleHook equivalent
constructor stateuseState(initial)
componentDidMountuseEffect(() => { ... }, [])
componentDidUpdateuseEffect(() => { ... }, [value])
componentWillUnmountthe function returned from useEffect
shouldComponentUpdateReact.memo around the component
static contextTypeuseContext(MyContext)
instance fields (this.timer)useRef()

Context: contextType versus useContext

Context is the most common reason a class needs “a hook”. It does not: classes have had static contextType for years. The limitation is that a class can read only one context that way, while a function component can call useContext as many times as it needs:

import { Component, createContext, useContext } from "react";

const ThemeContext = createContext("light");

// a class reads context with static contextType (one context only)
class ClassBadge extends Component {
  static contextType = ThemeContext;

  render() {
    return <span style={{ fontFamily: "system-ui" }}>class sees: {this.context}</span>;
  }
}

// a function component uses the hook, and can read as many contexts as it likes
function HookBadge() {
  const theme = useContext(ThemeContext);
  return <span style={{ fontFamily: "system-ui", marginLeft: 16 }}>hook sees: {theme}</span>;
}

export default function App() {
  return (
    <ThemeContext.Provider value="dark">
      <div style={{ padding: 16 }}>
        <ClassBadge />
        <HookBadge />
      </div>
    </ThemeContext.Provider>
  );
}
A class component reading a theme context with static contextType next to a function component reading it with useContext, both showing dark
Both read the same provider: the class through this.context, the function through the hook.

Should you convert everything?

  • Converting is worth it when you are already changing the component, when it needs two or more contexts, or when the lifecycle methods have grown into a tangle of unrelated concerns.
  • It is not worth it for a stable class that works, and class components are not deprecated: React 19 still supports them.
  • Error boundaries are the one thing that still needs a class. There is no hook for componentDidCatch.
  • Do not half-convert. A class with a wrapper for every hook it needs is harder to read than the function component it should have become.

More React guides worth reading:

Frequently asked questions

Can I use React hooks in a class component?

No. Hooks only work in function components and custom hooks. Calling one from a class throws an invalid hook call error at render time.

Can I call useState inside a class component?

No. Use this.state and this.setState in a class, or move the state into a function component that wraps it.

How do I pass hook data to a class component?

Wrap the class in a function component that calls the hook and passes the values, and any callbacks, down as props.

Can I use a custom hook with a class component?

Only indirectly. Call the custom hook in a wrapper function component and hand its return value to the class as props.

What replaces componentDidMount with hooks?

useEffect(() => { ... }, []). The function it returns replaces componentWillUnmount.

Are class components deprecated in React 19?

No. They still work and are still supported. Error boundaries in particular still have to be classes.

Why do I get an invalid hook call error?

Either the hook is in a class, inside a condition or loop, called from an event handler, or your project has two copies of React installed.

Leave a Comment