Skip to content

fix: keep useUniwind in sync when compiled by React Compiler - #719

Draft
dlebedynskyi wants to merge 1 commit into
uni-stack:mainfrom
alexa-endpoints:fix/use-uniwind-react-compiler
Draft

dlebedynskyi wants to merge 1 commit into
uni-stack:mainfrom
alexa-endpoints:fix/use-uniwind-react-compiler

Conversation

@dlebedynskyi

Copy link
Copy Markdown
Contributor

I ran into this in a monorepo where uniwind is a workspace package, so React Compiler compiles its source along with the app. With the compiler applied to src, the native tests showed useUniwind stuck on the first theme: ScopedTheme › useUniwind still read light after setTheme('dark'). Claude was fixing it.

When uniwind's source is compiled by React Compiler, useUniwind keeps returning the theme from its first render. Uniwind.setTheme('dark') re-renders the component, but theme stays light.

const Probe = () => {
    const { theme } = useUniwind()

    return <Text>{theme}</Text>
}

// renders "light"
Uniwind.setTheme('dark')
// re-renders, still "light"

useSnapshot reads Uniwind.currentTheme during render. The compiler treats getSnapshot() as pure and caches it on getSnapshot, which is a module-level function and never changes (babel-plugin-react-compiler 1.0.0):

const useSnapshot = (subscribe, getSnapshot) => {
  const $ = _c(9);
  const [snapshot, rerender] = useReducer(getSnapshot, undefined, getSnapshot);
  let t0;
  if ($[0] !== getSnapshot) {
    t0 = getSnapshot();
    $[0] = getSnapshot;
    $[1] = t0;
  } else {
    t0 = $[1];
  }
  const currentSnapshot = t0;
  // ...
  return currentSnapshot;
};

The theme listener still fires and rerender() runs, but the hook returns the cached $[1]. This affects native and web, since useUniwind.ts is shared.

The fix adds 'use no memo' to useSnapshot, as useStyle already does. Without the compiler the directive does nothing.

I also checked the other hooks that read runtime state under the compiler. useResolveClassNames and the withUniwind dependency hooks read the store only in reducers and effects, so they stay correct when compiled. useCSSVariable has the same render-time read, but the compiler skips it ("Reassigning initialized after render has completed"), so it isn't affected today. I can add the directive there too if you'd prefer to be explicit.

Tests

jest.config.native.js now runs the native suite as two projects: native (unchanged) and native-react-compiler, with src compiled by babel-plugin-react-compiler. The plugin is added as a devDependency at 1.0.0, the version bun.lock already has through babel-preset-expo. The only lockfile change is that devDependency line. Without the fix, native-react-compiler fails:

  • ScopedTheme › useUniwind: expected dark, received light
  • freeze › external stores catch up after a suspended tree is revealed: expected dark, received light

With the fix, both projects pass, 203/203 tests each. The web freeze test fails the same way under the compiler and passes with the fix. I checked that locally with a throwaway Vitest config that isn't part of this PR.

When it happens

Normal installs aren't affected. React Compiler's default sources skips paths that contain node_modules, and babel-preset-expo doesn't run the compiler on node modules either. It happens when the compiler reaches uniwind's source:

  • uniwind resolves outside node_modules, for example a workspace package in a monorepo or a symlinked local checkout
  • the app widens the compiler's sources to include node_modules

useUniwind reads Uniwind.currentTheme and Uniwind.hasAdaptiveThemes during
render through useSnapshot. When uniwind's source goes through
babel-plugin-react-compiler (a workspace-linked copy, or a compiler
`sources` option that includes node_modules), the compiler caches the
getSnapshot() call on getSnapshot, a module-level function that never
changes. The theme subscription still re-renders the component, but the
hook keeps returning the value from its first render, so useUniwind
reports the initial theme after Uniwind.setTheme on native and web.

Opt useSnapshot out with 'use no memo', as useStyle already does. Without
the compiler the directive does nothing.

The native Jest config now also runs the suite with src compiled by
babel-plugin-react-compiler 1.0.0, which bun.lock already resolves through
babel-preset-expo. Without the fix, that run fails `ScopedTheme ›
useUniwind` and `freeze › external stores catch up after a suspended tree
is revealed`.
@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant