From 6c217a1b489e61b7cae5cd67803ab21a63f4fd35 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 24 Oct 2026 10:11:20 -0700 Subject: [PATCH 1/6] docs(react-native): rewrite README for npm readers Put requirements and installation first and move the monorepo build scripts to a closing section. Document that iOS JS-thread globals are opt-in via init("", { globals: true }), Android release metadata filtering, and the Android steps of configure and the Expo plugin. Fix examples that touched UIKit or undefined names off the UI runtime. --- packages/react-native/README.md | 491 ++++++++++++++++++-------------- 1 file changed, 277 insertions(+), 214 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 4a38a7a1d..07c6be083 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -1,25 +1,171 @@ # @nativescript/react-native -React Native TurboModule wrapper for the NativeScript Native API JSI bridge on -Hermes. +NativeScript's Native API for React Native: call Objective-C and UIKit on iOS, +and Java and the Android SDK on Android, directly from JavaScript. -The module exposes one small TurboModule whose `init()` method attaches the +The package is one small TurboModule. Its `init()` method attaches the NativeScript Native API host object to `globalThis.__nativeScriptNativeApi` and -installs lazy NativeScript-style globals for classes and C functions. The host -object itself is pure JSI and is shared with the NativeScript Hermes runtime. +installs lazy NativeScript-style globals for native classes, functions, +constants and enums. The host object itself is pure JSI and is shared with the +NativeScript runtime. + +## Requirements + +- React Native 0.79 or newer. +- iOS 13+, with Hermes and the New Architecture. +- `react-native-worklets` 0.8 or newer. It is required on iOS: the pod links + `RNWorklets`, and UIKit work runs on the Worklets UI runtime. +- Android works with Hermes or JSC. See [Android](#android). + +Expo Go cannot load this package because it contains custom native code. Use an +Expo development build, EAS Build, or `npx expo run:ios` / `npx expo run:android`. + +## Installation + +### React Native + +1. Install the package and Worklets: + + ```sh + npm install @nativescript/react-native react-native-worklets + ``` + +2. Configure the project: + + ```sh + npx nativescript-rn configure + ``` + + `configure` adds the NativeScript and Worklets Babel plugins when missing, + writes `nativescript.react-native.json`, applies the Android Gradle script, + adds the Android typings to `tsconfig.json`, and warns when iOS is not + configured for Hermes and the New Architecture. It is intentionally + conservative and does not make destructive native project edits. + + To configure Babel by hand instead: + + ```js + module.exports = { + presets: ["module:@react-native/babel-preset"], + plugins: [ + "@nativescript/react-native/babel-plugin", + ["react-native-worklets/plugin", { strictGlobal: true }], + ], + }; + ``` + + `strictGlobal: true` is required. Without it the Worklets plugin captures + every name a worklet does not declare (`UIView`, `UILabel`, ...) into the + worklet's closure, read on the JavaScript thread when the module defining + the worklet is evaluated. NativeScript's classes do not exist there, so the + app fails at startup with `ReferenceError: Property 'UILabel' doesn't exist`. + With `strictGlobal`, those names are resolved on the runtime the worklet runs + on. With `babel-preset-expo`, also pass `{ worklets: false }` to the preset + so it does not add a second copy of the plugin without `strictGlobal`. + +3. Install the pods: + + ```sh + cd ios + RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install + ``` + +`npx nativescript-rn generate-metadata --check` prints the iOS metadata +configuration (extra pods and system frameworks) that native builds will use. + +### Expo + +1. Install the package and Worklets: + + ```sh + npx expo install @nativescript/react-native react-native-worklets + ``` + +2. Add the config plugin to `app.json` or `app.config.js`: + + ```json + { + "expo": { + "plugins": ["@nativescript/react-native"] + } + } + ``` + + The plugin: + + - configures iOS for Hermes and the React Native New Architecture, which this + JSI TurboModule requires; + - applies the NativeScript Gradle script to `android/app/build.gradle`; + - adds the `@nativescript/react-native/babel-plugin` and + `react-native-worklets/plugin` (with `{ strictGlobal: true }`) transforms to + `babel.config.js`, and passes `{ worklets: false }` to `babel-preset-expo` + so the preset does not add its own copy of the Worklets plugin. An existing + `react-native-worklets/plugin` entry that already has options is left + alone, with a warning if it lacks `strictGlobal: true`. + + Set `{ "babelPlugin": false }` in the plugin options if you prefer to add the + NativeScript and Worklets Babel plugins manually. + +3. Prebuild and run a development build: + + ```sh + npx expo prebuild + npx expo run:ios + npx expo run:android + ``` + +The plugin also writes `nativescript.react-native.json` so metadata options are +visible to native builds. On iOS you can pass metadata inputs when the app uses +Objective-C-visible pods or extra system frameworks: + +```json +{ + "expo": { + "plugins": [ + [ + "@nativescript/react-native", + { + "metadata": { + "includePods": ["SomeObjCSDK"], + "includeSystemFrameworks": ["UIKit", "MapKit", "WebKit"] + } + } + ] + ] + } +} +``` + +For Android release builds, `metadata.android.include` lists classes to keep; +see [Android setup](#setup). Bare projects using `nativescript-rn configure` can +put the same `metadata` object under a `nativeScriptReactNative` key in +`package.json`. An `android` block written into `nativescript.react-native.json` +by hand is kept unless the options set one. + +Importing the package in a build without its native module is safe: +`NativeScript.isAvailable()` returns `false` there, and `init()` and the other +functions that need the module throw an error saying what to rebuild. + +## Usage + +Call `init()` once, before using native APIs: ```ts import NativeScript from "@nativescript/react-native"; NativeScript.init(); - -const object = NSObject.new(); ``` -`NativeScript.init()` also installs the Native API into the -`react-native-worklets` UI runtime. `NativeScript.runOnUI()` only accepts -Worklets callbacks; running React Native's JS-thread runtime as a UI-thread shim -is not supported. +### Which runtime gets the globals (iOS) + +UIKit must only be touched on the main thread, and React Native's JavaScript +runs on its own JS thread. So on iOS, `init()` installs the full set of native +globals (`UIView`, `NSObject`, `NSTextAlignment`, ...) into the +`react-native-worklets` UI runtime, which runs on the main thread. On the +JavaScript thread it installs only `globalThis.__nativeScriptNativeApi`, +`interop`, and protocol globals. + +Run UIKit work through `NativeScript.runOnUI()` with a Worklets callback: ```ts await NativeScript.runOnUI(() => { @@ -28,26 +174,34 @@ await NativeScript.runOnUI(() => { }); ``` -Install `react-native-worklets`, add its Babel plugin, and run `pod install` so -the `RNWorklets` pod is linked: +`runOnUI()` only accepts Worklets callbacks and throws when the callback was not +transformed into a Worklets function; running React Native's JS-thread runtime +as a UI-thread shim is not supported. `installWorklets()` is still exported for +custom initialization, but it throws when Worklets is unavailable or +incompatible. -```sh -npm install react-native-worklets -``` +For non-UI APIs on the JavaScript thread (Foundation, file system, networking), +either look classes up explicitly or opt into JS-thread globals: -```js -module.exports = { - presets: ["module:@react-native/babel-preset"], - plugins: [ - "@nativescript/react-native/babel-plugin", - "react-native-worklets/plugin", - ], -}; +```ts +const NSDateClass = NativeScript.getClass("NSDate"); + +// or +NativeScript.init("", { globals: true }); +const now = NSDate.new(); ``` -`installWorklets()` is still exported for custom initialization, but it throws -when Worklets is unavailable or incompatible. `runOnUI()` throws when the -callback was not transformed into a Worklets function. +Only full C constant names become globals, for example `NSTextAlignmentCenter` +and `NSOrderedAscending`; short enum member names stay on their enum +(`NSTextAlignment.Center`). JS-thread globals make it possible to touch UIKit +off the main thread by accident, which is why they are opt-in. Helpers that +build native classes, such as `createDelegate()`, need them when called on the +JavaScript thread. + +On Android, `init()` installs the Java package namespaces (`java`, `android`, +...) on the JavaScript runtime. See [Android](#android). + +### Callbacks and threads Obj-C blocks and JS-backed Obj-C method callbacks, including `NSObject.extend` subclass overrides and delegates created with `createDelegate()`, should return @@ -55,13 +209,17 @@ to React Native's JS thread for JS work. Use `jsInvoker()` when a callback can b reached from a native caller thread: ```ts -UIView.animateWithDurationAnimationsCompletion( - 0.25, - null, - NativeScript.jsInvoker((finished) => { - console.log("animation finished", finished); - }), -); +NativeScript.init("", { globals: true }); + +const url = NSURL.URLWithString("https://nativescript.org"); +NSURLSession.sharedSession + .dataTaskWithURLCompletionHandler( + url, + NativeScript.jsInvoker((data, response, error) => { + console.log("request finished", error == null); + }), + ) + .resume(); ``` Delegate, data-source, target/action, and `UIAction` callbacks are JS-side @@ -69,7 +227,7 @@ callbacks. Treat their bodies as JS work. If a callback can be reached from a background native thread and needs to mutate UIKit, wrap the mutation in `NativeScript.runOnUI()` with a Worklets callback. -The package also includes a Babel plugin for directive-style JS callbacks: +The bundled Babel plugin also supports directive-style JS callbacks: ```ts someNativeApi(() => { @@ -145,11 +303,11 @@ badgeRef.current?.invalidateNativeLayout(); React Native view props such as `style`, `testID`, accessibility props, responder props, and `pointerEvents` go to the host component. Your own props go to the -UIKit definition; use `nativeProps(props)` when a plugin prop should also affect -the RN host. The `name` option is forwarded to the shared native host view as a -debug name, so native view descriptions can show `NativeScriptUIView` with your -definition name. It does not dynamically change the registered RN host component -tag. +UIKit definition; add a `nativeProps(props)` function to the definition when a +plugin prop should also affect the RN host. The `name` option is forwarded to the +shared native host view as a debug name, so native view descriptions can show +`NativeScriptUIView` with your definition name. It does not dynamically change +the registered RN host component tag. ### Lifecycle and context @@ -239,7 +397,9 @@ retainer.dispose(); `createDelegate(protocols, methods, options)` accepts protocol objects or names. If metadata was generated before a framework was loaded, use strings with -`NativeScript.loadFramework()` and `NativeScript.getProtocol()`: +`NativeScript.loadFramework()` and `NativeScript.getProtocol()`. Passing +`{ owner: ctx }` from a definition's `create(ctx)` ties the delegate to the +component's lifetime: ```ts NativeScript.loadFramework("QuickLook"); @@ -251,7 +411,7 @@ const dataSource = NativeScript.createDelegate( return 1; }, previewControllerPreviewItemAtIndex() { - return NSURL.fileURLWithPath(path); + return NSURL.fileURLWithPath(ctx.props.path); }, }, { owner: ctx }, @@ -397,12 +557,13 @@ export const NativeTabs = NativeScript.defineUIViewController< ``` For modal UIKit controllers, find the top visible presenter and guard against -double presentation: +double presentation. Helpers called from a worklet must be worklets too: ```ts function topVisibleViewController( root = UIApplication.sharedApplication.keyWindow?.rootViewController, ) { + "worklet"; let current = root; while (current?.presentedViewController) { current = current.presentedViewController; @@ -440,7 +601,6 @@ if ( const CameraController = NativeScript.getClass< typeof VNDocumentCameraViewController >("VNDocumentCameraViewController"); - const controller = CameraController?.new(); } ``` @@ -461,168 +621,11 @@ to JS errors where Objective-C can catch them. Process-level failures such as violations are not catchable; use availability checks and presentation guards instead of relying on exceptions as control flow. -The package ships example definitions under `@nativescript/react-native/examples`. - -The published package includes generated NativeScript metadata, the libffi -xcframework, and generated iOS SDK TypeScript declarations. Build it from the -repository root with: - -```sh -npm run build-rn-turbomodule -``` - -The tarball is written to `packages/react-native/dist/` and copied to -`build/npm-tarballs/`. - -To verify it inside a generated React Native iOS app: - -```sh -npm run test-rn-turbomodule -``` - -## Using the package in a React Native app +More example definitions ship in the package's `examples/` folder, for example +`@nativescript/react-native/examples/UIKitSwitch`. -1. Build or download the package tarball. -2. Install it in an RN app that has Hermes and the New Architecture enabled: - - ```sh - npm install /path/to/nativescript-react-native-0.0.1.tgz react-native-worklets - cd ios - RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install - ``` - -3. Initialize it before using native APIs: - - ```ts - import NativeScript from "@nativescript/react-native"; - - NativeScript.init(); - - await NativeScript.runOnUI(() => { - "worklet"; - UIApplication.sharedApplication.keyWindow.tintColor = - UIColor.systemPinkColor; - }); - ``` - -4. Add the bundled NativeScript Babel plugin and the Worklets Babel plugin: - - ```js - module.exports = { - presets: ["module:@react-native/babel-preset"], - plugins: [ - "@nativescript/react-native/babel-plugin", - "react-native-worklets/plugin", - ], - }; - ``` - -## Using the package in an Expo app - -Expo Go cannot load this package because it contains custom native code. Use an -Expo development build, EAS Build, or `npx expo run:ios`. - -1. Install the package: - - ```sh - npx expo install @nativescript/react-native react-native-worklets - ``` - - When testing a local tarball: - - ```sh - npm install /path/to/nativescript-react-native-0.0.1.tgz - ``` - -2. Add the config plugin to `app.json` or `app.config.js`: - - ```json - { - "expo": { - "plugins": ["@nativescript/react-native"] - } - } - ``` - - The plugin configures iOS for Hermes and the React Native New Architecture, - which are required by this JSI TurboModule. It also adds the - `@nativescript/react-native/babel-plugin` and `react-native-worklets/plugin` - transforms to `babel.config.js` so `"use js"` and worklet callbacks work in - Expo bundles. - -3. Prebuild and run the iOS development build: - - ```sh - npx expo prebuild --platform ios - npx expo run:ios - ``` - -4. Initialize NativeScript in app code before using native APIs: - - ```tsx - import NativeScript, { defineUIKitView } from "@nativescript/react-native"; - - NativeScript.init(); - - const NativeBadge = defineUIKitView<{ title: string }, UIView>({ - name: "NativeBadge", - create() { - const view = UIView.alloc().initWithFrame(CGRectZero); - const label = UILabel.alloc().initWithFrame(CGRectZero); - label.tag = 1; - label.textAlignment = NSTextAlignment.Center; - view.addSubview(label); - return view; - }, - update(view, props) { - view.backgroundColor = UIColor.systemBlueColor; - const label = view.viewWithTag(1) as UILabel; - label.text = props.title; - }, - }); - ``` - -Set `{ "babelPlugin": false }` in the config plugin options if you prefer to add -the NativeScript and Worklets Babel plugins manually. - -The plugin also writes `nativescript.react-native.json` so metadata options are -visible to native builds. You can pass metadata inputs when the app uses -Objective-C-visible pods or extra system frameworks: - -```json -{ - "expo": { - "plugins": [ - [ - "@nativescript/react-native", - { - "metadata": { - "includePods": ["SomeObjCSDK"], - "includeSystemFrameworks": ["UIKit", "MapKit", "WebKit"] - } - } - ] - ] - } -} -``` - -## Bare React Native setup helper - -The tarball includes a small CLI for bare RN projects: - -```sh -npx nativescript-rn configure -npx nativescript-rn generate-metadata --check -cd ios -RCT_NEW_ARCH_ENABLED=1 USE_HERMES=1 pod install -``` - -`configure` adds the bundled NativeScript and Worklets Babel plugins when -missing, writes -`nativescript.react-native.json`, and warns when the app is not configured for -Hermes and the New Architecture. The command is intentionally conservative and -does not make destructive native project edits. +The package includes generated NativeScript metadata, the libffi xcframework, +and generated iOS SDK TypeScript declarations. ## Android @@ -631,9 +634,9 @@ Android gives you Java and the Android SDK — plus every library your app depends on, because the metadata is generated from your app's own classpath. ```ts -import NativeScriptNativeApi from "@nativescript/react-native/src/NativeScriptNativeApi"; +import NativeScript from "@nativescript/react-native"; -NativeScriptNativeApi.install(""); +NativeScript.init(); const now = java.lang.System.currentTimeMillis(); const sdk = android.os.Build.VERSION.SDK_INT; @@ -650,8 +653,8 @@ prebuilt binary could not be safe across them. ### Setup -`npx nativescript-rn configure` adds the Gradle script and the typings entries. -To do it by hand, in `android/app/build.gradle`, after the React plugin: +`npx nativescript-rn configure` and the Expo config plugin both add the Gradle +script. To do it by hand, in `android/app/build.gradle`, after the React plugin: ```groovy apply from: "../../node_modules/@nativescript/react-native/android/nativescript.gradle" @@ -659,19 +662,57 @@ apply from: "../../node_modules/@nativescript/react-native/android/nativescript. That adds two things to your build: -* **Metadata** — a description of every class on the app's resolved classpath, +* **Metadata** — a description of the classes on the app's resolved classpath, generated into the APK's assets on every build. This is what makes `android.widget.Toast` and any third-party library resolvable from JavaScript. `whitelist.mdg` / `blacklist.mdg` next to `android/app/build.gradle` filter it, with the same format the NativeScript CLI uses. -* **TypeScript declarations** — off by default because they take a while: + + Debug builds ship the whole classpath. Release builds ship only what the + JavaScript bundle can reach, plus the supertypes, nested classes and + signature types those classes need. They also leave out the packages of + autolinked React Native modules, which are reached over the TurboModule + bridge rather than the interop. A class your code only reaches dynamically + (a name built at runtime, for example) can therefore resolve in debug and be + missing in release. Declare such classes in `nativescript.react-native.json` + at the project root, as `"pkg.Class"`, `"pkg.*"` or `"pkg:Class"`: + + ```json + { + "reactNative": { + "metadata": { + "android": { + "include": ["com.example.sdk.Worker", "com.example.views.*"] + } + } + } + } + ``` + + With Expo, put the same `android` object under `metadata` in the config + plugin options. `-PnsFilterMetadata=false` turns the filter off and ships + the whole classpath. +* **TypeScript declarations** — for the same classpath, so third-party + libraries are typed too. They are written to `types/android/` next to the app + on the first build and regenerated only when the classpath changes; commit + them alongside your code. `-PnsGenerateTypings=false` opts out, and ```bash - cd android && ./gradlew generateNativeScriptTypingsDebug -PnsGenerateTypings=true + cd android && ./gradlew generateNativeScriptTypingsDebug + ``` + + generates them on their own. `configure` adds them to `tsconfig.json`; with + the Expo config plugin, add them yourself: + + ```json + { + "include": ["types/android/android.d.ts", "types/android/android-declarations.d.ts"] + } ``` - which writes `types/android/` and, with `configure` having run, is already - referenced from `tsconfig.json`. + The package does not ship Android declarations, and `@nativescript/types-android` + is not needed: it covers only the Android SDK, and its declarations collide + with the generated ones. ### Extending Java classes @@ -775,6 +816,12 @@ you. The one thing to avoid is blocking the JS thread while waiting for such a callback — `thread.join()` immediately after `thread.start()` deadlocks, because the callback cannot run until the JS thread is free again. +That includes callbacks Android runs on the main thread: a `Runnable` posted to +`new Handler(Looper.getMainLooper())` runs its JavaScript on the JS thread. There +is no Worklets UI runtime on Android yet, so `runOnUI()` and the UIKit view +helpers are iOS only, and touching an `android.view.View` from JavaScript throws +`CalledFromWrongThreadException`. Every non-UI Android API is available. + ### What a guest does not install React Native already provides `console`, timers, `queueMicrotask`, @@ -782,3 +829,19 @@ React Native already provides `console`, timers, `queueMicrotask`, installs only the metadata globals, the object manager and the interop callbacks. `Worker` is not installed either: a worker needs its own VM, which a guest has no way to create. + +## Building from source + +From the root of the [NativeScript/runtimes](https://github.com/NativeScript/runtimes) +repository: + +```sh +npm run build-rn-turbomodule +``` + +The tarball is written to `packages/react-native/dist/` and copied to +`build/npm-tarballs/`. To verify it inside a generated React Native iOS app: + +```sh +npm run test-rn-turbomodule +``` From dc29b60d291ee41c89b07d1cf3052a6fc5fc895d Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 24 Oct 2026 10:11:46 -0700 Subject: [PATCH 2/6] docs(react-native): document the $name fallback for Java package globals --- packages/react-native/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 07c6be083..a6aa47197 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -642,6 +642,16 @@ const now = java.lang.System.currentTimeMillis(); const sdk = android.os.Build.VERSION.SDK_INT; ``` +Each top-level Java package is a global named after it (`java`, `android`, +`androidx`, `com`, ...). A package whose name is a JavaScript keyword gets a `$` +prefix instead. So does one whose name the host has already defined as a +read-only global, and a warning is logged. Expo defines `expo`, for example, so +Expo's own Java classes are under `$expo`: + +```ts +const modules = $expo.modules; +``` + ### How it works The runtime does not bring a JavaScript engine. It binds to the From fde5d0bd3dd9d3df462038ba0c8c0e7e5f415bae Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sat, 24 Oct 2026 11:59:16 -0700 Subject: [PATCH 3/6] docs(react-native): describe Android behaviour across a JavaScript reload --- packages/react-native/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index a6aa47197..853994f85 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -832,6 +832,25 @@ is no Worklets UI runtime on Android yet, so `runOnUI()` and the UIKit view helpers are iOS only, and touching an `android.view.View` from JavaScript throws `CalledFromWrongThreadException`. Every non-UI Android API is available. +### Reloads + +A reload (Fast Refresh's full reload, `DevSettings.reload()`, or +`expo-updates`' `reloadAsync()`) creates a new JavaScript runtime. NativeScript +lets go of the old one and installs its globals into the new one, so +`NativeScript.init()` works the same after a reload. + +Java objects that the old runtime created or extended stay bound to it. If one of +them calls back into JavaScript after the reload, for example a delayed `Runnable` +or a listener that was never removed, the call throws: + +``` +Cannot call com.tns.gen.java.lang.Runnable.run(): the JavaScript runtime it belonged to was reloaded or shut down. +``` + +If this happens on the main thread, the app crashes. To avoid it, remove +listeners and cancel posted callbacks when the component that registered them +unmounts. + ### What a guest does not install React Native already provides `console`, timers, `queueMicrotask`, From d77be3234be98e405d785f3a2a2f8bb1ca9189ce Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 25 Oct 2026 13:23:28 -0700 Subject: [PATCH 4/6] docs(react-native): match the reworked missing-module and package-global behaviour --- packages/react-native/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 853994f85..aae2bc964 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -142,9 +142,9 @@ put the same `metadata` object under a `nativeScriptReactNative` key in `package.json`. An `android` block written into `nativescript.react-native.json` by hand is kept unless the options set one. -Importing the package in a build without its native module is safe: -`NativeScript.isAvailable()` returns `false` there, and `init()` and the other -functions that need the module throw an error saying what to rebuild. +Importing the package in a build without its native module (Expo Go, or a build +made before the package was added) throws an error that says to use a +development build or rebuild the app. ## Usage @@ -644,9 +644,9 @@ const sdk = android.os.Build.VERSION.SDK_INT; Each top-level Java package is a global named after it (`java`, `android`, `androidx`, `com`, ...). A package whose name is a JavaScript keyword gets a `$` -prefix instead. So does one whose name the host has already defined as a -read-only global, and a warning is logged. Expo defines `expo`, for example, so -Expo's own Java classes are under `$expo`: +prefix instead. So does one whose name is already a global when NativeScript +installs, and a warning is logged. Expo defines `expo`, for example, so Expo's +own Java classes are under `$expo`: ```ts const modules = $expo.modules; From 42c9ac3492181bac2d1ddb6a456c90ec3364b83f Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 25 Oct 2026 13:43:56 -0700 Subject: [PATCH 5/6] docs(react-native): quote the reworked reload callback error --- packages/react-native/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index aae2bc964..8737690a2 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -841,10 +841,11 @@ lets go of the old one and installs its globals into the new one, so Java objects that the old runtime created or extended stay bound to it. If one of them calls back into JavaScript after the reload, for example a delayed `Runnable` -or a listener that was never removed, the call throws: +or a listener that was never removed, the call throws a `NativeScriptException`, +the same as a callback into a terminated worker: ``` -Cannot call com.tns.gen.java.lang.Runnable.run(): the JavaScript runtime it belonged to was reloaded or shut down. +com.tns.NativeScriptException: Cannot find runtime for instance=com.tns.gen.java.lang.Runnable@708609e ``` If this happens on the main thread, the app crashes. To avoid it, remove From 8fb4fb39128e041580a635b97e88f7b08852de3f Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 25 Oct 2026 14:13:52 -0700 Subject: [PATCH 6/6] docs(react-native): runOnUI returns native objects of the caller's runtime --- packages/react-native/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-native/README.md b/packages/react-native/README.md index 8737690a2..c560ca27e 100644 --- a/packages/react-native/README.md +++ b/packages/react-native/README.md @@ -180,6 +180,12 @@ as a UI-thread shim is not supported. `installWorklets()` is still exported for custom initialization, but it throws when Worklets is unavailable or incompatible. +Native objects the worklet returns, at the top level or inside plain arrays and +objects, come back as objects of the React Native runtime. Each runtime has its +own wrapper for a native object, and using a wrapper from the other runtime +throws. To hand one across yourself, pass `interop.handleof(object).toHexString()` +and turn it back with `interop.object(handle)`. + For non-UI APIs on the JavaScript thread (Foundation, file system, networking), either look classes up explicitly or opt into JS-thread globals: