Skip to content

fix(ios): blank RiveView after Fabric recreates the component view (react-freeze) — props skipped by consumed isDirty flags - #365

Open
rlods wants to merge 3 commits into
rive-app:mainfrom
rlods:fix/ios-remount-props-not-applied
Open

fix(ios): blank RiveView after Fabric recreates the component view (react-freeze) — props skipped by consumed isDirty flags#365
rlods wants to merge 3 commits into
rive-app:mainfrom
rlods:fix/ios-remount-props-not-applied

Conversation

@rlods

@rlods rlods commented Aug 18, 2026

Copy link
Copy Markdown

Symptom

On iOS (new architecture), a RiveView renders permanently blank after navigating away from its screen and coming back, when the app uses react-native-screens with enableFreeze(true) (react-freeze / Suspense). The native view is laid out (background color, size all correct) but nothing is ever drawn, and play() / playIfNeeded() on the ref silently do nothing. Android is unaffected.

Root cause

When a screen is frozen, Fabric deletes its native views; on return it recreates them from the same, unchanged ShadowNodes — calling updateProps:oldProps: on the brand-new component view with the same cached Props object as before.

The nitrogen-generated HybridRiveViewComponent.mm guards every prop with isDirty and, after applying it, mutates the shared props object (newViewProps.file.isDirty = false; via const_cast). Those flags were already consumed by the previous view instance, so on recreation updateProps applies nothing:

  • the fresh HybridRiveView keeps its default empty HybridRiveFile, so afterUpdate() bails at guard let file = hybridFile.riveFile else { return }configure() never runs, no RiveView subview is ever created (we confirmed with a view-hierarchy inspector: the recreated RiveReactNativeView has zero children);
  • hybridRef never re-fires, so JS still holds the ref to the dead old hybrid — which is why play() resolves fine but does nothing (baseViewModel?.play() on nil).

Nothing ever marks the props dirty again unless the JS file object changes identity, so the view stays blank forever.

This is the mirror image of mrousavy/nitro#1050: there, a recycled view keeps the old props; here, a recreated view gets no props at all. Reproduced on @rive-app/react-native@0.4.19; the generated code is identical on main and in 0.5.0-beta.1.

Fix

Track _didApplyInitialProps on the component view and force-apply every prop on the first updateProps of each view instance, keeping the isDirty fast path for all subsequent updates. First-ever mounts are unaffected (all flags are dirty there anyway); recreated views now get their full configuration and hybridRef fires with the new hybrid.

We've been running this as a patch in our app (large health-insurance app, several Rive scenes behind frozen tab/stack screens) and it fixes the blank-view repro deterministically.

Note on the generated file

I'm aware this file is nitrogen-generated (DO NOT MODIFY) — the durable fix likely belongs in nitrogen's view-component template (and would then cover frameRate/semantics/onStop and any future props automatically, as this PR's force || does). Opening this here since the generated sources are committed to this repo and ship in the npm package; happy to close in favor of an upstream nitrogen fix if you prefer to route it there.

Repro

  1. RN app with new architecture + react-native-screens, enableFreeze(true)
  2. Screen A renders a <RiveView>; push any screen B on top (freeze unmounts A's native views)
  3. Go back to A → the recreated RiveView is blank; play() no-ops; only a file identity change revives it

🤖 Generated with Claude Code

@rlods

rlods commented Aug 18, 2026

Copy link
Copy Markdown
Author

cc @rive-engineering @HayesGordon for review 🙏
cc @mrousavy if you had a better fix suggestion to be applied here or in nitro?
cc @simontreny @romainbraun @xseignard for info

@rlods

rlods commented Aug 18, 2026

Copy link
Copy Markdown
Author

@mfazekas

mfazekas commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

@rlods Thanks for the detailed write-up — I reproduced this and your diagnosis is spot on.

Note it also reproduces with a plain display: 'none' toggle on the parent View — no
react-native-screens, no react-freeze. Same Fabric delete/recreate, same blank view.
Android is unaffected, as you said.

Your patch does fix it (verified on both paths, iOS harness green), but it modifies a generated
file, and CI regenerates nitrogen/generated/ and fails on any diff. Two options:

  • Upstream fixed this in nitrogen/nitro 0.37.0 (feat: Track Nitro View prop identity mrousavy/nitro#1503 + #1506 + #1510) — props are now diffed against oldProps instead of mutating shared isDirty state. I'd go this way.
  • Move the force || into scripts/nitrogen-postprocess.ts, which already patches generated C++ for nitro#1184, so it survives yarn nitrogen.

@rlods

rlods commented Aug 22, 2026

Copy link
Copy Markdown
Author

Upstream fixed this in nitrogen/nitro 0.37 (...) I'd go this way.

thanks @mfazekas i'm totally aligned that upstream fix is the way to go

my PR was just a light temporary fix until nitro was upgraded in this repo which has much bigger impact

do you plan to handle the nitro upgrade ?

in the meantime, I'll integrate second option in this PR (I didn't know about nitrogen-postprocess script)

rlods and others added 2 commits August 22, 2026 19:53
When Fabric recreates a component view from an unchanged ShadowNode (e.g.
react-freeze/Suspense re-inserting a previously hidden screen), the shared
Props object's isDirty flags were already consumed by the previous view
instance, so updateProps applied nothing: the fresh HybridRiveView never
received file/artboardName/hybridRef and rendered blank forever.

Force-apply every prop on a view instance's first updateProps, keeping the
isDirty fast path for subsequent updates.
…urvives regeneration

CI regenerates nitrogen/generated/ and fails on any diff, so the
force-apply-on-first-updateProps patch cannot live only in the generated
HybridRiveViewComponent.mm. Apply it from scripts/nitrogen-postprocess.ts
(alongside the existing nitro#1184 patch) so `yarn nitrogen` reproduces
the committed file byte-for-byte.

Also make acceptNullForOptionalProps idempotent: it used to re-insert its
null-check lines when the script ran twice without regenerating first.

The patch is fixed upstream in nitro 0.37 (mrousavy/nitro#1503, #1506,
mrousavy/nitro#1510) — both postprocess patches can be dropped on the next
nitro upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rlods
rlods force-pushed the fix/ios-remount-props-not-applied branch from 8836352 to 6450982 Compare August 22, 2026 18:02
The top-level guard already returns when the file contains the
value.isNull() marker, so the inner re-check could never be true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants