You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
varResolver: a var() resolved from the :root/inline/universal fallback isn't guarded — a token provided via context after first render never re-resolves #377
In varResolver (src/native/styles/variables.ts), a re-render dependency guard is recorded only on the name in variables (inherited VariableContext) path. When a var() is instead resolved from the inline / universal / :root fallback, the function returns without pushing a guard. As a result, a token that is absent from context at first render (so it resolves from :root) and is later provided via a nearer VariableContextProvider is never re-resolved on an already-mounted node — the node stays at the :root/fallback value permanently.
It's timing-sensitive: it only manifests when the context value arrives after the first paint of the consuming node. On a fast first paint the token is already in context (guarded → works); on a slower first paint (e.g. Android/Hermes committing before an async provider updates) the node latches the fallback value forever.
Version
react-native-css@3.0.7
Repro
A styled/useCssElement node whose var(--x) starts unset (resolves from :root) and is later given a value via a VariableContextProvider higher in the tree keeps the :root value — it never re-resolves when --x subsequently appears in context.
Concretely: render a node using var(--brand) where --brand is not yet provided by any context; later push --brand into a VariableContextProvider. The mounted node keeps the :root/fallback value instead of updating.
Root cause
varResolver pushes renderGuards.push(["v", name, variables[name]]) inside the if (name in variables) { … } branch and then returns. The other path — reached when name is not in variables (it runs variableHistory.add(name) and then the inline → universal → :root → fallback lookups) — returns without recording any guard for name. So testGuards watches nothing for that token, and a later context change for it never triggers re-resolution of the mounted node.
Proposed fix
Record a guard for the "currently absent from context" state as well — push renderGuards.push(["v", name, variables[name]]) immediately after variableHistory.add(name) (before the fallback lookups). variables[name] is undefined there, which is the correct sentinel: the guard then fires precisely when the token later appears in context, and there is no value shadow (the returned value is still the :root/fallback until context provides one). It terminates cleanly — exactly one extra re-resolve on the absent→present transition.
Context
We hit this in a production React Native app where channel/brand CSS variables are supplied asynchronously (fetched, then injected as a delta over the :root defaults). iOS happened to paint after the fetch (token in context → guarded → correct); Android painted before it and latched the default. We're currently carrying a local patch that applies the one-line fix above; filing upstream so it can live in the library. Happy to open a PR if that's useful.
(Distinct from #245, which is about a memory leak on re-render.)
Reproduced the bug via a Jest unit test. A node whose var(--brand) initially resolves from :root (returning #f00) does not re-resolve when --brand is later injected via a VariableContextProvider higher in the tree — the node stays at the :root value (#f00) instead of updating to the new context value (#00f). Root-cause analysis in the issue is accurate: varResolver in src/native/styles/variables.ts:49-52 only pushes a renderGuard on the name in variables branch; the fallback path (inline → universal → :root → fallback) returns without recording a guard for the absent-from-context state, so testGuards never observes the later absent→present transition.
What I tested
Read src/native/styles/variables.ts and traced the guard-push paths against the reported code path.
Wrote a minimal Jest repro: initial render with :root { --brand: red } and .test { color: var(--brand) }, then a state-driven rerender that wraps the same node in <VariableContextProvider value={{ "--brand": "blue" }}>.
Result: post-update style was { color: "#f00" } instead of the expected { color: "#00f" } — confirms the latch on the :root fallback.
Confirmed current published version is 3.0.7 (matches reporter).
Next steps
Reporter's proposed one-line fix — pushing renderGuards.push(["v", name, variables[name]]) (i.e. undefined) immediately after variableHistory.add(name) in src/native/styles/variables.ts:54 — matches the guard semantics used elsewhere in the resolver and should cause exactly one extra re-resolve on the absent→present transition without a stale-value shadow.
Reporter offered a PR; worth accepting given they've already vetted it in production.
Consider adding a regression test covering this transition (initial :root-only → later VariableContextProvider injection) alongside the existing VariableContextProvider test in src/__tests__/native/variables.test.tsx.
Activity
github-actions commentedon Jul 15, 2026
🤖 Auto-triage
Status: CONFIRMED
Type: bug
Version: v5 (react-native-css@3.0.7)
Findings
Reproduced the bug via a Jest unit test. A node whose
var(--brand)initially resolves from:root(returning#f00) does not re-resolve when--brandis later injected via aVariableContextProviderhigher in the tree — the node stays at the:rootvalue (#f00) instead of updating to the new context value (#00f). Root-cause analysis in the issue is accurate:varResolverinsrc/native/styles/variables.ts:49-52only pushes arenderGuardon thename in variablesbranch; the fallback path (inline → universal →:root→ fallback) returns without recording a guard for the absent-from-context state, sotestGuardsnever observes the later absent→present transition.What I tested
src/native/styles/variables.tsand traced the guard-push paths against the reported code path.:root { --brand: red }and.test { color: var(--brand) }, then a state-driven rerender that wraps the same node in<VariableContextProvider value={{ "--brand": "blue" }}>.{ color: "#f00" }instead of the expected{ color: "#00f" }— confirms the latch on the:rootfallback.3.0.7(matches reporter).Next steps
renderGuards.push(["v", name, variables[name]])(i.e.undefined) immediately aftervariableHistory.add(name)insrc/native/styles/variables.ts:54— matches the guard semantics used elsewhere in the resolver and should cause exactly one extra re-resolve on the absent→present transition without a stale-value shadow.:root-only → laterVariableContextProviderinjection) alongside the existingVariableContextProvidertest insrc/__tests__/native/variables.test.tsx.This is an automated triage. See auto-triage.yml.