From 76ae9dd2f643271b5ed7734fedeecacc9f826eba Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Wed, 2 Sep 2026 11:03:08 +0200 Subject: [PATCH 1/3] fix: drop frozen listeners --- packages/uniwind/src/hoc/withUniwind.native.tsx | 11 +++++++++-- .../src/hooks/useCSSVariable/useCSSVariable.ts | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/uniwind/src/hoc/withUniwind.native.tsx b/packages/uniwind/src/hoc/withUniwind.native.tsx index 8453d3c3..b4d1cc95 100644 --- a/packages/uniwind/src/hoc/withUniwind.native.tsx +++ b/packages/uniwind/src/hoc/withUniwind.native.tsx @@ -1,5 +1,5 @@ import type { ComponentProps } from 'react' -import { useCallback, useSyncExternalStore } from 'react' +import { useCallback, useLayoutEffect, useReducer } from 'react' import type { StyleDependency } from '../common/consts' import { isDefined } from '../common/utils' import { useUniwindContext } from '../core/context' @@ -25,8 +25,15 @@ const useDependencies = (dependencies: Array) => { () => UniwindListener.getSnapshot(uniqueDependencies), [dependencySum], ) + const [snapshot, rerender] = useReducer(getSnapshot, undefined, getSnapshot) - useSyncExternalStore(subscribe, getSnapshot, getSnapshot) + useLayoutEffect(() => { + if (getSnapshot() !== snapshot) { + rerender() + } + + return subscribe(rerender) + }, [subscribe, getSnapshot]) } export const withUniwind: WithUniwind = < diff --git a/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts b/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts index 9f6f9925..abb621a6 100644 --- a/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts +++ b/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts @@ -1,4 +1,4 @@ -import { useMemo, useRef, useSyncExternalStore } from 'react' +import { useLayoutEffect, useMemo, useReducer, useRef } from 'react' import { Platform } from 'react-native' import { StyleDependency } from '../../common/consts' import { arrayEquals } from '../../common/utils' @@ -93,5 +93,15 @@ export const useCSSVariable: GetCSSVariable = (name: string | Array) => } }, [stableName, uniwindContext]) - return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) as never + const [snapshot, rerender] = useReducer(getSnapshot, undefined, getSnapshot) + + useLayoutEffect(() => { + if (getSnapshot() !== snapshot) { + rerender() + } + + return subscribe(rerender) + }, [subscribe, getSnapshot]) + + return snapshot as never } From fbd1e94970ceb2e49b2bfb276d11434ea8cde3bd Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Wed, 2 Sep 2026 11:20:55 +0200 Subject: [PATCH 2/3] chore: return current snapshot in useCSSVariable --- .../hooks/useCSSVariable/useCSSVariable.ts | 3 ++- .../components/scoped-variables.test.tsx | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts b/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts index abb621a6..454977eb 100644 --- a/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts +++ b/packages/uniwind/src/hooks/useCSSVariable/useCSSVariable.ts @@ -94,6 +94,7 @@ export const useCSSVariable: GetCSSVariable = (name: string | Array) => }, [stableName, uniwindContext]) const [snapshot, rerender] = useReducer(getSnapshot, undefined, getSnapshot) + const currentSnapshot = getSnapshot() useLayoutEffect(() => { if (getSnapshot() !== snapshot) { @@ -103,5 +104,5 @@ export const useCSSVariable: GetCSSVariable = (name: string | Array) => return subscribe(rerender) }, [subscribe, getSnapshot]) - return snapshot as never + return currentSnapshot as never } diff --git a/packages/uniwind/tests/native/components/scoped-variables.test.tsx b/packages/uniwind/tests/native/components/scoped-variables.test.tsx index 75ce02d3..26c3b263 100644 --- a/packages/uniwind/tests/native/components/scoped-variables.test.tsx +++ b/packages/uniwind/tests/native/components/scoped-variables.test.tsx @@ -105,6 +105,33 @@ describe('ScopedVariables', () => { expect(inside).toHaveBeenCalledWith('#123456') }) + test('useCSSVariable reflects a changed variable name during render', () => { + const seen: Array = [] + const variables = { '--color-primary': '#3b82f6', '--color-secondary': '#ff0000' } + + const Probe = ({ name }: { name: string }) => { + seen.push(useCSSVariable(name)) + + return null + } + + const Wrapper = ({ name }: { name: string }) => ( + + + + ) + + const { rerender } = renderUniwind() + const renderCount = seen.length + + act(() => { + rerender() + }) + + expect(seen.slice(renderCount)).not.toContain('#3b82f6') + expect(seen.at(-1)).toEqual('#ff0000') + }) + test('useCSSVariable reflects an updated variables prop', () => { const seen: Array = [] From b88ae7754ac0d9f6d043134ee13fa8f86f0a204b Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Wed, 2 Sep 2026 11:27:55 +0200 Subject: [PATCH 3/3] chore: remove useSyncExternalStore --- packages/uniwind/src/hoc/withUniwind.tsx | 12 ++++++++++-- packages/uniwind/src/hooks/useUniwind.ts | 22 ++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/uniwind/src/hoc/withUniwind.tsx b/packages/uniwind/src/hoc/withUniwind.tsx index 41154129..d7c07d8b 100644 --- a/packages/uniwind/src/hoc/withUniwind.tsx +++ b/packages/uniwind/src/hoc/withUniwind.tsx @@ -1,5 +1,5 @@ import type { ComponentProps } from 'react' -import { useCallback, useSyncExternalStore } from 'react' +import { useCallback, useLayoutEffect, useReducer } from 'react' import { isDefined } from '../common/utils' import { generateDataSet } from '../components/web/generateDataSet' import { useUniwindContext } from '../core/context' @@ -20,7 +20,15 @@ const useClassNames = (classNames: string) => { [classNames], ) - useSyncExternalStore(subscribe, getSnapshot, getSnapshot) + const [snapshot, rerender] = useReducer(getSnapshot, undefined, getSnapshot) + + useLayoutEffect(() => { + if (getSnapshot() !== snapshot) { + rerender() + } + + return subscribe(rerender) + }, [subscribe, getSnapshot]) } export const withUniwind: WithUniwind = < diff --git a/packages/uniwind/src/hooks/useUniwind.ts b/packages/uniwind/src/hooks/useUniwind.ts index 37f6ff74..f7cdda8b 100644 --- a/packages/uniwind/src/hooks/useUniwind.ts +++ b/packages/uniwind/src/hooks/useUniwind.ts @@ -1,4 +1,4 @@ -import { useSyncExternalStore } from 'react' +import { useLayoutEffect, useReducer } from 'react' import { StyleDependency } from '../common/consts' import { Uniwind } from '../core' import { useUniwindContext } from '../core/context' @@ -11,14 +11,28 @@ const subscribeToNothing = () => () => {} const getTheme = () => Uniwind.currentTheme const getHasAdaptiveThemes = () => Uniwind.hasAdaptiveThemes +const useSnapshot = (subscribe: (callback: () => void) => () => void, getSnapshot: () => T) => { + const [snapshot, rerender] = useReducer(getSnapshot, undefined, getSnapshot) + const currentSnapshot = getSnapshot() + + useLayoutEffect(() => { + if (getSnapshot() !== snapshot) { + rerender() + } + + return subscribe(rerender) + }, [subscribe, getSnapshot]) + + return currentSnapshot +} + export const useUniwind = (): { theme: ThemeName; hasAdaptiveThemes: boolean } => { const uniwindContext = useUniwindContext() const isScoped = uniwindContext.scopedTheme !== null - const theme = useSyncExternalStore(isScoped ? subscribeToNothing : subscribeToTheme, getTheme, getTheme) - const hasAdaptiveThemes = useSyncExternalStore( + const theme = useSnapshot(isScoped ? subscribeToNothing : subscribeToTheme, getTheme) + const hasAdaptiveThemes = useSnapshot( isScoped ? subscribeToNothing : subscribeToAdaptiveThemes, getHasAdaptiveThemes, - getHasAdaptiveThemes, ) return {