Skip to content

fix: Treat null as undefined for removed optional View props - #1515

Open
mrousavy wants to merge 1 commit into
mainfrom
fix/view-optional-prop-removal
Open

fix: Treat null as undefined for removed optional View props#1515
mrousavy wants to merge 1 commit into
mainfrom
fix/view-optional-prop-removal

Conversation

@mrousavy

@mrousavy mrousavy commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

  • React's Fabric diff sends an explicit null sentinel for a removed prop (ReactNativeAttributePayload: "Flag the leaf property for removal by sending a sentinel") - and prop={undefined} takes the same path, so the two are indistinguishable on the wire
  • CachedProp previously passed that null straight into conversion, which throws mid-commit for every optional prop type that rejects null (optional<double> via asNumber(), and hybridRef even earlier, in the { f } function-prop unwrap)
  • now: if the prop type is optional and null is not a valid value for it (JSIConverter<T>::canConvert), the value is normalized to undefined → the native setter is called once with nullopt

Semantics

  • optional prop removed / set to undefined → setter receives nullopt (the JS-visible value becomes undefined). The native initializer default still only covers never-provided props - a richer "reset on clear" belongs in the implementation (storage ?? default)
  • props that can legitimately hold null (NullType, variants containing null): untouched - null stays data, so removal collapses to "explicit null" for those (inherent to React's sentinel protocol)
  • required props: unchanged, removal still throws loudly with the ViewName.propName: prefix

Because normalization only applies where conversion was guaranteed to throw, no currently-working behavior can change.

Overhead

  • non-optional props: none - the if constexpr (is_optional<T>) branch compiles out
  • optional props: one value.isNull() tag check during Props parsing; the canConvert walk only runs when the value actually is null (i.e. an actual removal)

Testing

New Harness test delivers undefined when an optional prop is removed": mounts TestView with nativeDefaultValue={1}+hybridRef, then removes both - asserts the value reads undefined, the setter fired exactly once more, unrelated setters (isBlue) did not fire, and the previously-crashing hybridRef` removal path survives. Red without the fix (commit-time throw), green with it.

Validated: workspace typecheck, eslint (CI settings), clang-format.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
nitro-docs Skipped Skipped Aug 21, 2026 10:11am

Request Review

React Fabric diff turns a removed prop into an explicit null sentinel (and prop={undefined} takes the same path). Nitro ReactProp passed that null straight to JSIConverter, which throws for non-null types, so removing any previously-provided optional View prop (including hybridRef, which crashed earlier in the { f } unwrap) failed the Fabric commit.

Now, if the prop type is optional and null is not a valid value for it (checked via JSIConverter<T>::canConvert), the value is normalized to undefined and the setter is called once with nullopt. Props that can legitimately hold null (NullType, variants containing null) are untouched, and required props still throw loudly. Zero overhead for non-optional props (the branch compiles out), one isNull() tag check for optional ones.
@mrousavy
mrousavy force-pushed the fix/view-optional-prop-removal branch from 737d1c7 to 089bc77 Compare August 21, 2026 10:11
mfazekas added a commit to rive-app/rive-nitro-react-native that referenced this pull request Aug 23, 2026
Generated updateProps now diffs the new Props snapshot against the old one
instead of consuming isDirty flags that live on the shared Props object
(margelo/nitro#1503, #1506, #1510), so a component view Fabric recreates
from an unchanged ShadowNode gets every provided prop applied again rather
than nothing. That is what view-recreate.harness.tsx was written for. The
peer range moves with it, so consumers have to bump too.

Prop parsing also moved into Nitro core, which leaves the post-process shim
for margelo/nitro#1184 without a parse site to patch — but that bug is still
live, so clearing an optional prop throws mid-commit ("RiveView.
layoutScaleFactor: Value is null, expected a number"). Carry mrousavy's own
fix, margelo/nitro#1515, as a yarn patch until it ships, and cover both the
optional<double> and the variant shape with a harness test. The generated
EventPropertiesOutput variant is now ordered `boolean | number | string`, so
the two hand-written helpers swap .second and .third.

Both example apps make glog non-modular in post_install: 0.37.0 exposes
React's renderer headers in the NitroModules modulemap and they reach
<glog/logging.h>, whose headers include from inside `namespace google` —
illegal once glog is a module, so every target that builds the NitroModules
module fails to compile. Nothing in React Native imports glog as a module.
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.

Error when assigning undefined to optional view property

1 participant