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)React Compiler
Migrate useCallStateHooks() usage to be compatible with React Compiler's static analysis requirements.
Best Practices
- Call
useCallStateHooks()at module scope, not inside components. - Alias the import:
import { useCallStateHooks as getCallStateHooks }. - Destructure hooks at module level, use them normally in components.
- Use
@stream-io/video-codemodfor automated migration.
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.
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
- Alias the import to avoid naming confusion (import as
getCallStateHooks) - Call the factory once at module scope
- Destructure hooks at the top level
- 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 use-call-state-hooks codemod on your codebase:
npx @stream-io/video-codemod use-call-state-hooks ./src \
--extensions=ts,tsx \
--parser=tsx \
--run-prettier # optional, runs prettier on the transformed files