npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rn-org/react-native-thread

v0.8.3

Published

Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses Hermes on both iOS and Android, each on a dedicated OS-level thread. Built as a New Architecture TurboModule.

Readme

@rn-org/react-native-thread

Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses Hermes on both iOS and Android, each on a dedicated OS-level thread. Built as a New Architecture TurboModule.


Contents


Features

  • True background threads — each thread runs its own isolated Hermes runtime on a dedicated OS-level thread; the main React Native runtime is never blocked. Works on both iOS and Android.
  • Unlimited threads — create as many threads as you need; each is isolated.
  • Shared thread (runOnJS) — fire-and-forget tasks on a single persistent background thread; no teardown required.
  • Promise-based run()thread.run(fn, params) returns a Promise that resolves with the value passed to resolve(data) and rejects on uncaught thread errors. Chain .then() / .catch() or use await.
  • Parameter injection — pass values from the main thread into the background function as (args, resolve) => { ... }. Supports primitives, objects, arrays, and functions.
  • resolve as second param — the callback to send a result back is passed directly as the second argument to your task function — no globals needed.
  • Function params — pass functions (including imported ones) alongside plain data in the params object. The Babel plugin extracts their source at compile time and captures closed-over variables transitively.
  • Cross-module function params — functions imported from other files (e.g. import { compute } from './math') are automatically resolved and inlined by the Babel plugin.
  • Named threads — give threads friendly names; list or destroy them by name.
  • Error handling — thread exceptions are automatically caught and forwarded to the main thread via the rejected Promise.
  • Full console supportconsole.log/info/warn/error/debug work inside threads and appear in Logcat / Xcode logs.
  • Timer supportsetTimeout, setInterval, clearTimeout, and clearInterval work inside threads.
  • Hermes-safe — ships a Babel plugin that extracts function source at compile time so Hermes bytecode never breaks serialisation. Required on both iOS and Android since both platforms use Hermes.
  • New Architecture only — built on the TurboModule / Codegen pipeline.

Requirements

| | Minimum | |---|---| | React Native | 0.76+ (New Architecture) | | iOS | 15.1 | | Android | API 24 | | Node | 18+ |


Installation

npm install @rn-org/react-native-thread
# or
yarn add @rn-org/react-native-thread

iOS — run pod install:

cd ios && pod install

The hermes-engine pod is declared as a dependency in the library's podspec; no extra configuration needed.

Android — the Hermes native dependency is declared in the library's build.gradle; no extra steps needed.


Babel plugin (required for Hermes)

Hermes compiles your JS to bytecode at build time. That means fn.toString() at runtime returns a placeholder ("function () { [bytecode] }") instead of source code. The library includes a Babel plugin that:

  1. Extracts the task function (first argument) source at compile time and replaces it with a string literal.
  2. Detects function-valued properties in the params object (second argument) and inlines their source.
  3. Captures closed-over variables transitively — if a function references outer const/let/var bindings (literals or other functions), those are bundled into a self-contained IIFE.
  4. Resolves cross-module imports — functions imported from relative paths (e.g. import { fn } from './utils') are read from disk and inlined.
  5. Strips TypeScript annotations automatically when the source file is .ts or .tsx.

Add the plugin to your app's babel.config.js:

// babel.config.js
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    '@rn-org/react-native-thread/babel-plugin',
    // ... your other plugins
  ],
};

The plugin is a no-op on non-Hermes builds (V8, etc.).

Note: The plugin safely handles cases where @react-native/babel-preset transforms async functions before the plugin's visitor fires. It falls back to the original source text via Babel's preserved AST positions.


Quick start

import { runOnJS, createThread, getThreads, destroyThread } from '@rn-org/react-native-thread';

// ── 1. Fire and forget on the shared background thread ──────────────────────
runOnJS((args) => {
  console.log('Limit is', args.limit);
}, { limit: 42 });

// ── 2. Create a named, persistent thread ────────────────────────────────────
const thread = createThread('MyThread');

// Send work + params — thread.run() returns a Promise
const result = await thread.run(
  (args, resolve) => {
    var sum = 0;
    for (var i = 0; i < args.limit; i++) sum += i;
    resolve({ sum });
  },
  { limit: 1_000_000 }
);
console.log(result); // { sum: 499999500000 }

// ── 2b. Pass functions as params ────────────────────────────────────────────
function multiply(a, b) {
  return a * b;
}

const product = await thread.run(
  (args, resolve) => {
    resolve(args.multiply(args.x, args.y));
  },
  { x: 6, y: 7, multiply }
);
console.log(product); // 42

// Async function params work too — the Babel plugin transforms async/await
// to generator-based code before sending to the Hermes thread runtime:
const isPrime = async (num) => {
  if (num <= 1) return 'Not Prime';
  for (let i = 2; i < num; i++) {
    if (num % i === 0) return 'Not Prime';
  }
  return 'Prime';
};

const verdict = await thread.run(
  async (args, resolve) => {
    const result = await args.isPrime(args.num);
    resolve(`${args.num} is ${result}`);
  },
  { num: 17, isPrime }
);
console.log(verdict); // '17 is Prime'

// Functions that close over outer variables work too:
const factor = 10;
const scale = (n) => n * factor;

const scaled = await thread.run(
  (args, resolve) => {
    resolve(args.scale(5)); // 50
  },
  { scale }
);

// Imported functions are also supported:
import { checkEvenOdd } from './utils';

const parity = await thread.run(
  (args, resolve) => {
    resolve(args.checkEvenOdd(args.num));
  },
  { num: 42, checkEvenOdd }
);

// Error handling — thread.run() rejects if the thread throws
try {
  const data = await thread.run((args, resolve) => {
    throw new Error('something went wrong');
  });
} catch (err) {
  console.warn(err.message); // 'something went wrong'
}

// Or with .then() / .catch()
thread
  .run(async (args, resolve) => {
    const result = await args.isPrime(args.num);
    resolve(`${args.num} is ${result}`);
  }, { num: 17, isPrime })
  .then((data) => console.log(data))
  .catch((err) => console.warn(err.message));

// ── 3. List all running threads ─────────────────────────────────────────────
console.log(getThreads());
// [{ id: 1, name: 'RNOrgThread' }, { id: 2, name: 'MyThread' }]

// ── 4. Clean up ─────────────────────────────────────────────────────────────
thread.destroy();               // by handle
destroyThread('MyThread');      // by name — same effect
destroyThread(2);               // by id   — same effect

API reference

runOnJS

function runOnJS(task: ThreadTask, params?: unknown): void

Runs task on the shared persistent background thread (named "RNOrgThread"). The thread is created on first use and lives for the lifetime of the module. Ideal for fire-and-forget work where you don't need the overhead of managing a dedicated thread.

| Parameter | Type | Description | |---|---|---| | task | ThreadTask | Function or code string to execute. | | params | unknown | Optional JSON-serialisable value passed as the first argument to the function. |


runOnNewJS

function runOnNewJS(task: ThreadTask, params?: unknown, name?: string): ThreadHandle

Creates a new isolated thread, immediately runs task on it, and returns a ThreadHandle. The thread persists until you call handle.destroy() or destroyThread(...).

| Parameter | Type | Description | |---|---|---| | task | ThreadTask | Function or code string to execute. | | params | unknown | Optional value passed as the first argument to the function. | | name | string | Optional display name. Default: RNThread-<id>. |


createThread

function createThread(name?: string): ThreadHandle

Creates a new persistent named thread without immediately running any code. Use the returned ThreadHandle to dispatch work at any time.

| Parameter | Type | Description | |---|---|---| | name | string | Optional display name. Default: RNThread-<id>. |


getThreads

function getThreads(): ThreadInfo[]

Returns a snapshot of every live thread currently managed by the library, including the runOnJS shared thread once it has been started. Threads destroyed via destroyThread or handle.destroy() are removed from this list immediately.

const threads = getThreads();
// [
//   { id: 1, name: 'RNOrgThread' },
//   { id: 2, name: 'MyThread' },
// ]

destroyThread

function destroyThread(idOrName: number | string): void

Destroys a thread and frees its resources. Accepts either the numeric thread ID or the thread's name. When a name is given, the first matching thread is destroyed.

This is the same function called by ThreadHandle.destroy().

destroyThread('MyThread');   // by name
destroyThread(2);            // by id

In __DEV__ mode a warning is logged if no matching thread is found.


onMessage

// Callback — fires on every message from any thread
function onMessage(
  handler: (data: unknown, threadId: number) => void
): () => void

// Promise — resolves once on the next message from any thread
function onMessage(): Promise<{ data: unknown; threadId: number }>

Global listener that fires whenever any thread calls resolveThreadMessage(data). Prefer ThreadHandle.onMessage if you want messages scoped to a single thread.

// Callback variant
const unsub = onMessage((data, threadId) => {
  console.log(`Thread ${threadId} sent:`, data);
});
unsub(); // remove listener

// Promise variant — awaits the next message from any thread
const { data, threadId } = await onMessage();
console.log(`Thread ${threadId} sent:`, data);

ThreadHandle

Object returned by createThread and runOnNewJS.

type ThreadHandle = {
  readonly id: number;
  readonly name: string;
  /** Runs task on this thread. Resolves with the value passed to resolve(), rejects on error. */
  run(task: ThreadTask, params?: unknown): Promise<unknown>;
  destroy(): void;
};

| Member | Description | |---|---| | id | Numeric ID assigned by the native layer. | | name | Display name provided at creation (or the default RNThread-<id>). | | run(task, params?) | Execute task on this thread. Returns a Promise that resolves with the value passed to resolve(data) inside the task, or rejects if the thread throws. Can be called multiple times. | | destroy() | Shut down the thread and remove it from the registry. Equivalent to calling destroyThread(handle.id). |


ThreadInfo

type ThreadInfo = {
  readonly id: number;
  readonly name: string;
};

Returned by getThreads().


ThreadTask

type ThreadTask =
  | ((args: any, resolve: (data: unknown) => void) => void)
  | string;

Either an arrow function / function expression (transformed by the Babel plugin) or a raw JS code string. When a function is used, params is passed as args (first argument) and the resolve callback as resolve (second argument). Call resolve(data) to return a value to the caller.


Thread globals

These globals are available inside every thread function:

resolveThreadMessage

declare function resolveThreadMessage(data: unknown): void

Sends data back to the main JS thread. The value is JSON-serialised in the background thread and JSON-parsed before reaching the onMessage handler. Must be JSON-serialisable (object, array, string, number, boolean, or null).

const data = await thread.run((args, resolve) => {
  resolve({ status: 'done', value: args.multiply * 2 });
}, { multiply: 21 });
console.log(data); // { status: 'done', value: 42 }

resolveThreadMessage is also available as a global inside the thread (useful for raw code strings), and is the same function that resolve points to.


console

console.log, .info, .warn, .error, and .debug are all available and route to:

  • iOSNSLog, visible in Xcode / Console.app; tagged [RNThread-<id>] [Hermes].
  • Androidandroid.util.Log, visible in Logcat; tagged RNThread-<id>.

Timers

setTimeout, clearTimeout, setInterval, and clearInterval are available inside threads.

Both iOS and Android use the same Hermes-based event loop: after the initial evaluation, the thread drains all pending timers (and microtasks) before returning. The thread blocks until all timers have fired or been cleared.

thread.run(() => {
  setTimeout(() => {
    resolveThreadMessage('delayed hello');
  }, 2000);
});

Note: The event loop blocks the thread until timers complete. Be careful with setInterval — the thread won't finish until the interval is cleared inside the thread.


__params__

declare const __params__: any

Injected by the library when you pass a second argument to run(), runOnJS(), or runOnNewJS(). The value is JSON-serialised on the main thread and prepended to the code string as var __params__ = <JSON>;.

You can access the params value in two ways:

// Option A — args/resolve callback parameters
await thread.run(
  (args, resolve) => {
    for (var i = 0; i < args.iterations; i++) {
      // ...
    }
    resolve('done');
  },
  { iterations: 50_000 }
);

// Option B — __params__ global + resolveThreadMessage global
// (works in both functions and raw code strings)
await thread.run(
  (args, resolve) => {
    for (var i = 0; i < __params__.iterations; i++) {
      // ...
    }
    resolve('done');
  },
  { iterations: 50_000 }
);

// Option C — raw code string (__params__ and resolveThreadMessage globals)
thread.run(
  'for (var i = 0; i < __params__.iterations; i++) {} resolveThreadMessage("done")',
  { iterations: 50_000 }
);

Hermes & Babel plugin deep dive

The problem

The Hermes compiler converts your JS bundle to bytecode at build time. Any function whose .toString() is called at runtime returns something like:

function () { [bytecode] }

Because this library must serialise functions to strings and send them to a separate JS engine, .toString() alone doesn't work under Hermes.

The solution

The included Babel plugin runs at compile time — before Hermes touches the code — and transforms both the task function and function-valued params.

Task function (first argument)

Arrow functions and function expressions (including async) are replaced with a string literal wrapped as an IIFE. async/await is transformed to generator-based code because Hermes' eval-mode compiler does not support async syntax. The generated call passes both __params__ and the global resolveThreadMessage so they arrive as args and resolve respectively:

// Input (your source)
thread.run(async (args, resolve) => {
  const result = await args.compute(args.num);
  resolve(result);
}, { num: 42, compute });

// Output (what Hermes compiles) — async transformed, compute inlined
thread.run("((function(){ ... _asyncToGenerator ... })())(__params__, resolveThreadMessage)", {
  num: 42,
  compute: { __rnThreadFn: "(function(){ ... })()" }
});

Function params (second argument)

Functions passed inside the params object are detected, extracted, and tagged with { __rnThreadFn: "<source>" }. The runtime serializer emits the source directly into the thread code.

// Input
const b = 3;
const add = (x) => x + b;
function multiply(a) {
  return add(a) * 2;
}
thread.run(fn, { multiply });

// Output — transitive closures are captured in a self-contained IIFE
thread.run(fn, {
  multiply: {
    __rnThreadFn: "(function(){ var b = 3;\nvar add = (x) => x + b;\nreturn function multiply(a) { return add(a) * 2; };})()" 
  }
});

Cross-module imports

Functions imported from relative paths are resolved from disk, parsed, and inlined:

// utils.ts
export function checkEvenOdd(num: number): string {
  return num % 2 === 0 ? 'Even' : 'Odd';
}

// App.tsx
import { checkEvenOdd } from './utils';
thread.run(fn, { checkEvenOdd });
// → checkEvenOdd is inlined with TypeScript stripped

Supported call sites

| Pattern | Task transformed | Params transformed | |---|---|---| | runOnJS((args) => { ... }, { fn }) | Yes | Yes | | runOnNewJS((args, resolve) => { ... }, { fn }) | Yes | Yes | | anyHandle.run((args, resolve) => { ... }, { fn }) | Yes | Yes | | Raw code string run("...", { fn }) | No (already a string) | Yes |

What the plugin captures in params

| Value type | Captured | |---|---| | Inline arrow / function expression | Yes | | Inline async arrow / function expression | Yes (transformed to generator) | | Reference to local function declaration | Yes | | Reference to local const fn = () => ... | Yes | | Reference to local const fn = async () => ... | Yes (transformed to generator) | | Imported function (import { fn } from './mod') | Yes (relative paths only) | | Closed-over const/let/var with literal init | Yes (transitively) | | Closed-over function references | Yes (transitively) | | Runtime-computed values, Map, Set, classes | No — pass as plain params |

Limitations

  • Captured closures must be statically resolvable: only variables initialized with literals and functions (local or imported) are captured. Runtime-computed values (e.g. const x = fetchValue()) must be passed explicitly as params.
  • async functions in params are supported — the plugin transforms them to generator-based code before extraction. await works inside both task functions and function params.
  • Cross-module resolution only follows relative imports (e.g. './utils'). Package imports (e.g. 'lodash') are not resolved.
  • undefined, Map, Set, and class instances are not serialisable as param values — use plain objects, arrays, strings, numbers, booleans, or null.
  • Thread functions run in an isolated Hermes runtime with no access to the React tree, native modules, or the main thread's global scope.

Constraints summary

| Constraint | Reason | |---|---| | Thread functions run in isolation | They execute in a completely separate Hermes runtime (iOS and Android) | | Hermes eval-mode has no async/await | The Babel plugin transforms async to generators before extraction | | Function params must be statically resolvable | The Babel plugin extracts source at compile time | | Non-function params must be JSON-serialisable | Serialised via JSON.stringify at runtime | | resolveThreadMessage payload must be JSON-serialisable | Transported as a JSON string over the bridge | | Cross-module resolution is relative-only | The plugin reads files from disk using the import path | | No fetch / XMLHttpRequest | Thread runtimes have no network stack | | New Architecture required | TurboModule / Codegen only; no bridge fallback |


Contributing


License

MIT