Summary
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 happens to re-render or remount for an unrelated reason. Verified on uniwind-pro 1.6.0 (paying customer) and the mechanism is identical in OSS 1.11.0.
This is the report @Brentlok asked for in #466 ("If you have similar issue for oss version please create a separated issue with reproduction") — with the extra finding that Pro's suspended-trees support fixes the shadow-tree path but not the JS paths.
What works / what doesn't (observed on Pro 1.6.0, RN 0.85.3, Expo SDK 56, new arch)
Repro flow: tab A visible, tab B frozen (freezeOnBlur / <Freeze>), call Uniwind.setTheme, then focus tab B.
- ✅ className styles on built-in components: correct on the frozen tab — the suspended-trees support from 1.0.0-rc.7 works.
- ❌
withUniwind-wrapped components (vector icons, third-party text/number components): keep the old theme's colors.
- ❌ Accent props (
tintColorClassName on Image etc.): tinted glyphs keep the old tint. Notably the ref detach/re-attach cycle re-links the shadow node on thaw, but the stale render-time tintColor prop is what remains on screen — the relink doesn't catch the accent up to the current theme.
- ❌
useUniwind(): returns the pre-flip theme forever.
- ✅
useCSSVariable / useResolveClassNames: correct after thaw.
Everything stale self-repairs on the next setTheme while visible, or on remount — which makes the bug look intermittent in a real app (list recycling randomly heals some elements and not others).
Mechanism
react-freeze hides via Suspense. React tears down the hidden tree's layout effects on freeze and re-creates them on reveal without re-rendering. The three broken paths all resolve during render and subscribe in a useLayoutEffect whose body only re-subscribes:
hoc/withUniwind.native.tsx — generatedProps computed in render; effect only subscribes
hooks/useUniwind.ts — useState(Uniwind.currentTheme) snapshot; effect only subscribes
- component accents —
getAccentColor(...) in render; the missed notification is never replayed
The two hooks that heal do so because their effect body re-syncs on every run:
useResolveClassNames.native.ts calls recreate()
useCSSVariable.ts calls updateValue() (guarded by isMountRef)
The same gap also drops a change landing between initial render and the layout-effect subscribe.
Minimal reproduction
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>
)
}
Press 1 → 2 → 3. Expected: everything reflects the new theme after unfreezing. Actual: the useCSSVariable line and (on Pro) className styles are correct; the tinted image and the useUniwind line keep the old theme indefinitely. Same result with two tabs + freezeOnBlur: true, flipping from the other tab. A withUniwind(AnyIcon) wrapper behaves like the tinted image.
Suggested fix
Give the broken paths the same self-heal the healthy hooks already have — re-sync in the subscription effect body (no-op guarded on first mount, mirroring useCSSVariable's isMountRef), or move these subscriptions to useSyncExternalStore, which rides the passive-effect path React deliberately keeps connected under a suspended Suspense boundary. For accents, additionally catching the value up when a shadow node re-links would cover the native side.
Versions
- uniwind-pro: 1.6.0 (also applies to OSS uniwind 1.11.0 — hook sources identical on
main)
- react-native: 0.85.3, new architecture · Expo SDK 56 (
~56.0.12) · react 19
- react-freeze 1.0.4 · react-native-screens 4.25.2 · React Compiler enabled (relevant: element caching removes the accidental parent-re-render repair, making the staleness sticky)
- Platform: observed on Android emulator + iOS device builds
Summary
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 (tintColorClassNameetc.), anduseUniwind— until the affected component happens to re-render or remount for an unrelated reason. Verified on uniwind-pro 1.6.0 (paying customer) and the mechanism is identical in OSS 1.11.0.This is the report @Brentlok asked for in #466 ("If you have similar issue for oss version please create a separated issue with reproduction") — with the extra finding that Pro's suspended-trees support fixes the shadow-tree path but not the JS paths.
What works / what doesn't (observed on Pro 1.6.0, RN 0.85.3, Expo SDK 56, new arch)
Repro flow: tab A visible, tab B frozen (
freezeOnBlur/<Freeze>), callUniwind.setTheme, then focus tab B.withUniwind-wrapped components (vector icons, third-party text/number components): keep the old theme's colors.tintColorClassNameonImageetc.): tinted glyphs keep the old tint. Notably the ref detach/re-attach cycle re-links the shadow node on thaw, but the stale render-timetintColorprop is what remains on screen — the relink doesn't catch the accent up to the current theme.useUniwind(): returns the pre-flip theme forever.useCSSVariable/useResolveClassNames: correct after thaw.Everything stale self-repairs on the next
setThemewhile visible, or on remount — which makes the bug look intermittent in a real app (list recycling randomly heals some elements and not others).Mechanism
react-freeze hides via Suspense. React tears down the hidden tree's layout effects on freeze and re-creates them on reveal without re-rendering. The three broken paths all resolve during render and subscribe in a
useLayoutEffectwhose body only re-subscribes:hoc/withUniwind.native.tsx—generatedPropscomputed in render; effect only subscribeshooks/useUniwind.ts—useState(Uniwind.currentTheme)snapshot; effect only subscribesgetAccentColor(...)in render; the missed notification is never replayedThe two hooks that heal do so because their effect body re-syncs on every run:
useResolveClassNames.native.tscallsrecreate()useCSSVariable.tscallsupdateValue()(guarded byisMountRef)The same gap also drops a change landing between initial render and the layout-effect subscribe.
Minimal reproduction
Press 1 → 2 → 3. Expected: everything reflects the new theme after unfreezing. Actual: the
useCSSVariableline and (on Pro) className styles are correct; the tinted image and theuseUniwindline keep the old theme indefinitely. Same result with two tabs +freezeOnBlur: true, flipping from the other tab. AwithUniwind(AnyIcon)wrapper behaves like the tinted image.Suggested fix
Give the broken paths the same self-heal the healthy hooks already have — re-sync in the subscription effect body (no-op guarded on first mount, mirroring
useCSSVariable'sisMountRef), or move these subscriptions touseSyncExternalStore, which rides the passive-effect path React deliberately keeps connected under a suspended Suspense boundary. For accents, additionally catching the value up when a shadow node re-links would cover the native side.Versions
main)~56.0.12) · react 19