Skip to content

feat: Pass View callbacks to native directly on react-native 0.81+ - #1512

Draft
mrousavy wants to merge 1 commit into
mainfrom
feat/view-raw-function-props
Draft

feat: Pass View callbacks to native directly on react-native 0.81+#1512
mrousavy wants to merge 1 commit into
mainfrom
feat/view-raw-function-props

Conversation

@mrousavy

@mrousavy mrousavy commented Aug 20, 2026

Copy link
Copy Markdown
Member

Function props no longer need the { f: … } object workaround on react-native 0.81 and above.

Why this works now

react-native converts every function prop to true before it reaches native - unless the View Config declares a process function for that prop. That escape hatch landed in facebook/react#32119 / facebook/react-native#48777 and first shipped in react-native 0.81.0 (verified against the published packages: attributeConfigHasProcess is absent in 0.80.3, present in 0.81.0-rc.0):

// ReactNativeAttributePayload.js (react-native 0.81+)
if (typeof nextProp === 'function') {
  const attributeConfigHasProcess =
    typeof attributeConfig === 'object' &&
    typeof attributeConfig.process === 'function'
  if (!attributeConfigHasProcess) { nextProp = true;  }
}

Nitro already sets process: (i) => i on every attribute in wrapValidAttributes(...) (added in #853 for the custom diff), so on 0.81+ functions already arrive as raw jsi::Functions - the wrapper was just dead weight.

What changed

-<Camera onCaptured={callback((i) => console.log(i))} />
+<Camera onCaptured={(i) => console.log(i)} />

Both react-native versions keep a fast path - the version is resolved once, never per prop:

  • C++: a compile-time #if on REACT_NATIVE_VERSION_* (NITRO_RAW_FUNCTION_PROPS in RawPropsCompat.hpp). On 0.81+ the function-prop branch in CachedProp::fromRawValue(...) compiles away entirely and functions go straight into JSIConverter; on 0.78 - 0.80 the { f: … } object is unwrapped exactly as before.
  • JS: callback(...) reads Platform.constants.reactNativeVersion once, memoizes it, and returns the function as-is on 0.81+.

callback(...) is now @deprecated but still works everywhere, and View props accept both a raw function and a wrapped { f: … } object, so this is not a breaking change for apps on react-native 0.78 - 0.80 or for existing callback(...) call sites.

Also fixes a latent crash on react-native 0.78 - 0.80: react-native puts an explicit null in the props payload for a removed prop, which used to throw in value.asObject(...) while unwrapping f (e.g. when hybridRef goes from set to undefined).

Tests

  • Migrated the View harness tests to raw functions, so the whole existing View suite now covers the new path.
  • "still accepts callbacks wrapped in the deprecated callback(...)" - back-compat coverage for the wrapper.
  • "keeps updating props after an optional callback prop is removed" - regression test for the null case above.
  • Compile-checked CachedProp.hpp against react-native 0.85 headers with NITRO_RAW_FUNCTION_PROPS both 1 and 0.

Docs

"Callbacks have to be wrapped" is now "Callbacks", documenting raw functions as the default and callback(...) as the react-native 0.78 - 0.80 workaround.

Since react-native 0.81 (react/react-native#48777, react/react#32119),
a View Config can opt out of react-native converting function props to `true`
by declaring a `process` function for the prop - which Nitro already does for
every prop in `wrapValidAttributes(...)`.

That means functions now arrive on the native side as raw `jsi::Function`s, so
the `{ f: function }` object workaround (`callback(...)`) is no longer needed:

```diff
-<Camera onCaptured={callback((i) => console.log(i))} />
+<Camera onCaptured={(i) => console.log(i)} />
```

Both paths stay a fast path - the react-native version is resolved once, not
per prop:
- C++ uses a compile-time `#if` on `REACT_NATIVE_VERSION_*`
  (`NITRO_RAW_FUNCTION_PROPS`). On 0.81+ the function-prop branch compiles away
  entirely, on 0.78 - 0.80 the `{ f: … }` object is unwrapped as before.
- JS resolves `Platform.constants.reactNativeVersion` once and memoizes it.

`callback(...)` is now deprecated and a no-op on react-native 0.81+, and View
props accept both a raw function and a wrapped `{ f: … }` object, so existing
code keeps working on every supported react-native version.

Also fixes removing a function prop on react-native 0.78 - 0.80: react-native
sends an explicit `null` for removed props, which used to throw while
unwrapping `f`.
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
nitro-docs Ready Ready Preview Aug 20, 2026 2:12pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6df8ca543b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +101 to +104
#if NITRO_RAW_FUNCTION_PROPS
// Every prop - including functions - arrives as the raw JSI value it was
// in JS, so there is nothing to unwrap here.
return CachedProp<T>::fromJSIValue(*runtime, std::move(value), previousProp);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unwrap legacy callback objects on RN 0.81+

On React Native 0.81+, this unconditional raw-value path sends a legacy { f: function } wrapper into JSIConverter<std::function<...>>, whose fromJSI tries to cast the object itself to a function and throws. This conflicts with the newly exported AllowWrappedFunctions type, which explicitly accepts the wrapper, and breaks older JS bundles or callers constructing the public wrapper format after the native app is upgraded. The added compatibility test does not exercise this case on RN 0.81+ because the updated callback(...) helper returns the raw function there; this path should preserve raw functions while still detecting and unwrapping { f: ... } objects.

Useful? React with 👍 / 👎.

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