Skip to content

fix(native): warn about a re-mount exactly when the element re-mounts - #469

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/remount-warning-matches-remount
Open

YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/remount-warning-matches-remount

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

useNativeCss re-mounts an element only when the state it renders from changes its wrappers or its animated component, or swaps a View for a Pressable. updateRules decides whether to warn about a re-mount inside its loop over the element's className props, from what that loop has accumulated so far. That check disagrees with the re-mount in four ways:

  1. A component with more than one className prop (ScrollView's className and contentContainerClassName) compares the first prop's partial result. A scope that never changed logs className 'undefined' added or removed a variable, and a real change logs once per prop.
  2. vars() is merged into the variables after the check, so an element that keeps them logs on mount and on every re-render.
  3. Only a View is swapped for a Pressable. Yet the pressable warning is logged by a Text or a ScrollView that gains an :active rule, and by a View whose own onPress already made it a Pressable. A View whose own onPress comes or goes is re-mounted with no warning, because onPress is not a render guard.
  4. will-change-pressable registers :hover (p: { h: 1 }), so a View carrying it is still swapped, and re-mounted, when an :active rule arrives.

Solution

  • updateRules derives the next state first and compares it with the state it was given, so the warning reads the same fields useNativeCss renders from.
  • The warning names the className props that carry classes (contentContainerClassName 'my-padding').
  • An element that is never swapped keeps pressable undefined, as its initial state already encodes.
  • A View gets a presence guard on onPress, so the swap is seen whichever side causes it. The guard is a "p" guard, which re-derives when onPress comes or goes and not when its identity changes.
  • will-change-pressable registers :active.

Tests

Every case renders a child that counts its mounts, so each assertion compares the warnings with a re-mount that was observed:

  • A ScrollView with a variable, a colour, a container and an animation rule, each on either className prop:
    • keeping it, keeping it while the other prop changes, moving it to the other prop, or re-rendering unchanged logs nothing and re-mounts nothing;
    • adding or removing it logs once and re-mounts once;
    • a re-render after the change does not repeat it, and reverting it warns once more;
    • adding it beside a class on the other prop names both props.
  • A custom property compiled with the default options and with inlineVariables: false: the padding resolves the same, and the element warns and re-mounts only when the compiled rule carries a variable, read from the compiler's output.
  • The Pressable swap:
    • an :active rule
    • will-change-pressable
    • a View's own onPress arriving, leaving or already there
    • a Text and a ScrollView, which are never swapped
  • will-change-variable, -container and -animation on one className prop keep the element from re-mounting when the rule arrives on the other.
  • vars() on style and on contentContainerStyle, in vars.test.tsx.
  • A production build re-mounts without the warning.

On main, 80 of the 107 cases in the two files fail; all pass here. Eight mutations each fail their own cases:

  • will-change-pressable registering :hover
  • no onPress presence guard
  • a presence guard that never fires
  • pressable ignoring the element's own onPress
  • pressable derived for an element that is never swapped
  • no container warning
  • a warning naming one prop
  • a warning on the first render

Verification

On Windows with Node 26:

  • yarn lint clean
  • yarn typecheck clean
  • yarn test --maxWorkers=2 --coverage: 1435 passed, 4 failed. The 4 are the babel cases that also fail on main on this machine.
  • yarn build clean
  • yarn example expo export --platform web exported
  • nothing unstaged after either build

No existing issue tracks this. I searched for added or removed a variable, will-change-variable, will-change-pressable, re-mounted, contentContainerClassName and remount warning. nativewind/nativewind#1646 reports this warning on a class change but has no reproduction, so I can't tell whether it is one of these cases.

Merge order

This overlaps #391; I'll re-cut whichever of the two lands second.

It also shares lines with #412 (__tests__/native/vars.test.tsx, native/react/rules.ts), #423 (__tests__/native/vars.test.tsx), #431 (__tests__/native/vars.test.tsx), #436 (native-internal/style-collection.ts), #443 (__tests__/native/vars.test.tsx); whichever lands second rebases.

Base

Re-written on main (a5002c5).

@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/remount-warning-matches-remount branch from 72bf741 to 8e8bef0 Compare October 6, 2026 10:05
The warning compared each class prop's partial state inside the loop,
so it fired on renders that re-mount nothing and stayed silent on some
that do. It now compares the derived state on the four fields
useNativeCss picks the element's wrappers from. `will-change-pressable`
marks an :active rule, which is what keeps an element a Pressable, and
an onPress arriving or leaving re-derives the rules.

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