Skip to content

Theme changes are missed by frozen (react-freeze) subtrees in JS-resolved paths — withUniwind, accent props, useUniwind #649

Description

@invivek26

What happened?

Theme changes that fire while a subtree is frozen by react-freeze (react-navigation freezeOnBlur, manual <Freeze>) are permanently missed by every JS-resolved styling path — withUniwind, accent props (tintColorClassName etc.), and useUniwind — until the affected component re-renders or remounts for an unrelated reason.

Verified on uniwind-pro 1.6.0: the suspended-trees support from 1.0.0-rc.7 works — className styles on built-in components are correct on frozen screens after a flip. But the JS paths stay stale:

  • withUniwind-wrapped components (vector icons, third-party text components) keep the old theme's colors
  • accent props (tintColorClassName on Image) keep the old tint — the ref re-attach re-links the shadow node on thaw, but the stale render-time tintColor prop remains on screen; the relink doesn't catch the accent up
  • useUniwind() returns the pre-flip theme forever
  • useCSSVariable / useResolveClassNames are correct after thaw (they re-sync in their effect bodies)

Mechanism: react-freeze hides via Suspense; React tears down the hidden tree's layout effects and re-creates them on reveal without re-rendering. The three broken paths resolve during render and subscribe in a useLayoutEffect whose body only re-subscribes — a notification that fired while the effects were torn down is never replayed (hoc/withUniwind.native.tsx, hooks/useUniwind.ts; accents via getAccentColor in render). The two healthy hooks heal precisely because their effect body re-syncs (useResolveClassNames.native.tsrecreate(), useCSSVariable.tsupdateValue()). The same gap drops a change landing between initial render and subscribe.

Suggested fix: give the broken paths the same effect-body re-sync the healthy hooks have (guarded to no-op on first mount, mirroring useCSSVariable's isMountRef), or move the subscriptions to useSyncExternalStore, which rides the passive-effect path React keeps connected under a suspended Suspense boundary. For accents, additionally catch the value up when a shadow node re-links.

This is the report Brentlok asked for in #466 ("If you have similar issue for oss version please create a separated issue with reproduction") — the hook sources are identical in OSS 1.11.0, so it applies there too.

Steps to Reproduce

import { useState } from "react"
import { Button, Image, Text, View } from "react-native"
import { Freeze } from "react-freeze"
import { Uniwind, useCSSVariable, useUniwind } from "uniwind"

const Probe = () => {
  const { theme } = useUniwind()
  const background = useCSSVariable("--color-background")
  return (
    <View className="bg-background p-4">
      <Text className="text-foreground">className ink (heals — Pro engine)</Text>
      <Image source={someGlyph} tintColorClassName="accent-foreground" />
      {/* ^ stays on the old tint after thaw */}
      <Text className="text-foreground">useUniwind: {theme} (stays stale)</Text>
      <Text className="text-foreground">useCSSVariable: {String(background)} (heals)</Text>
    </View>
  )
}

export const Repro = () => {
  const [frozen, setFrozen] = useState(false)
  return (
    <View className="flex-1 items-center justify-center gap-4 bg-background">
      <Button title="1. freeze" onPress={() => setFrozen(true)} />
      <Button
        title="2. flip theme"
        onPress={() =>
          Uniwind.setTheme(Uniwind.currentTheme === "dark" ? "light" : "dark")
        }
      />
      <Button title="3. unfreeze" onPress={() => setFrozen(false)} />
      <Freeze freeze={frozen}>
        <Probe />
      </Freeze>
    </View>
  )
}
  1. Press "freeze", then "flip theme", then "unfreeze".
  2. Expected: everything reflects the new theme after unfreezing.
  3. Actual: the useCSSVariable line and className styles are correct; the tinted image and the useUniwind line keep the old theme indefinitely (further freeze/thaw cycles stay stale; only a remount, or another flip while visible, repairs them). A withUniwind(AnyIcon) behaves like the tinted image.
  4. Same result with two tabs + freezeOnBlur: true, flipping the theme from the other tab. In a real app the staleness looks intermittent because list recycling remounts some elements and not others (React Compiler element caching removes the parent-re-render repair, making it sticky).

Snack or Repository Link (Optional)

No response

Uniwind version

uniwind-pro 1.6.0

React Native Version

0.85.3

Platforms

iOS, Android

Expo

Yes

Additional information 〰

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions