React Compiler

In this guide, we’ll explain how to migrate components that use useCallStateHooks() to be compatible with React Compiler.

React Compiler introduces stricter constraints on how hooks can be used so that components can be analyzed and optimized at build time. These constraints are enforced during development by eslint-plugin-react-compiler, which reports errors for patterns that are incompatible with React Compiler.

Problem

When using React Compiler, calling useCallStateHooks() inside a component violates React Compiler’s static analysis requirements. These requirements are enforced during development by ESLint, which reports the error shown below.

ESLint: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks (react-compiler/react-compiler)

Why this breaks with React Compiler

useCallStateHooks() is not a React hook. It’s a factory function that returns hooks.

Calling it inside a component creates hook function references during render. Even if those references are stable, React Compiler cannot verify that at compile time. Because hook functions must be statically known and identical on every render, this pattern is rejected.

Solution

Call useCallStateHooks() once at module scope, then destructure the hooks you need. Use those hooks normally inside your components.

Migration pattern

  1. Alias the import to avoid naming confusion (import as getCallStateHooks)
  2. Call the factory once at module scope
  3. Destructure hooks at the top level
  4. Use the hooks normally inside components

What not to do

Do not call the factory inside the component:

import { useCallStateHooks as getCallStateHooks } from "@stream-io/video-react-sdk";

const MyComponent = () => {
  const hooks = getCallStateHooks(); // ❌ still dynamic, still triggers error
  const { useCallCallingState } = hooks;
  // ...
};

Before / After

Before:

import { useCallStateHooks } from "@stream-io/video-react-sdk";

const MyComponent = () => {
  const { useCallCallingState, useParticipants } = useCallStateHooks();
  const callingState = useCallCallingState();
  const participants = useParticipants();

  return (
    <div>
      <div>State: {callingState}</div>
      <div>Participants: {participants.length}</div>
    </div>
  );
};

After:

import { useCallStateHooks as getCallStateHooks } from "@stream-io/video-react-sdk";

const { useCallCallingState, useParticipants } = getCallStateHooks();

const MyComponent = () => {
  const callingState = useCallCallingState();
  const participants = useParticipants();

  return (
    <div>
      <div>State: {callingState}</div>
      <div>Participants: {participants.length}</div>
    </div>
  );
};

Why alias to getCallStateHooks

Aliasing useCallStateHooks to getCallStateHooks is required. If you call useCallStateHooks() at module scope without aliasing, ESLint will error because functions starting with use are treated as hooks and cannot be called outside of components. The alias makes it clear that you’re calling a factory function at module scope, not using a hook inside a component.

Automated Migration with Codemod

Instead of manually migrating your code, you can use the @stream-io/video-codemod package to automatically transform your codebase.

Usage

Run the codemod on your codebase:

npx @stream-io/video-codemod <transform-name> <path>

For React Compiler migration, use the use-call-state-hooks transform:

npx @stream-io/video-codemod use-call-state-hooks ./src --extensions=ts,tsx --parser=tsx