Run one pure calculation off the JavaScript thread
one/background runs CPU work away from the app’s JavaScript thread:
filtering large lists, parsing files, scoring moves. Write the
calculation once as a pure function; One bundles its dependency graph
for a shared native Worklets runtime and generates a module Web Worker
on web. This runs while the app is alive; OS background launch and
scheduling belong to background tasks.
On native every owner shares one one-background Worklets runtime and
serial queue. On web each owner gets its own worker. Server rendering
starts no worker and returns a null result.
// calculate.ts: synchronous pure JavaScript, including any imported helpersexport function countValues(input: { values: number[]; minimum: number }) { return input.values.filter((value) => value >= input.minimum).length}import { defineBackgroundComputation } from 'one/background'import { countValues } from './calculate'
export const count = defineBackgroundComputation(countValues)import { useMemo } from 'react'import { Text } from 'react-native'import { useBackgroundComputation } from 'one/background'import { count } from './countValues'
function ValueCount({ values, minimum }: { values: number[]; minimum: number }) { const input = useMemo(() => ({ values, minimum }), [values, minimum]) const calculation = useBackgroundComputation(count, input) const result = calculation.result return <Text>{result === null ? '…' : `${result.value} values`}</Text>}The definition is a module-scope variable wrapping one named function
imported from a relative module. The hook publishes only the latest
input’s result: null while pending, then { revision, value }.
One runs one calculation per owner at a time. Updates replace the owner’s single waiting input; an executing calculation can finish, but its obsolete result or error never publishes. An update withdraws the previously accepted result immediately. Obsolete queued requests are skipped before dispatch on native. Revisions increase within an owner, and a newly mounted or re-enabled owner starts again at revision one, so compare results within one owner, never across mounts.
Inputs and results must survive both structured clone and Worklets:
plain objects, arrays, and scalars. Pass everything the calculation
needs as input. Its module graph may hold pure helpers, constants, and
data, but it cannot import React, React Native, native modules, or Node
builtins, touch a DOM, capture component state, or return a promise.
One bundles the pure graph into a worklet, so helpers need no
individual worklet directives. Native apps need react-native-worklets
installed and linked. The One Vite bundler owns the transform,
including its default native bundler; Metro is not supported.
Keep definitions stable and memoize input objects: changing either
input identity or the definition starts fresh work. Passing null
disables the hook and releases its owner. getCurrent reads the
accepted result when an action runs, outside render. A calculation error
reaches the hook’s React error boundary; a non-React owner receives it
in its state callback instead.
Disposal terminates a web worker and withdraws publication rights on native. This is not cooperative cancellation: a running calculation still finishes, but nothing it produces ever publishes.
| Functions | Type | Description |
|---|---|---|
defineBackgroundComputation<Input, Output>(calculate: (input: Input) => Output) => BackgroundComputation<Input, Output>Wrap one pure function as a stable definition. | <Input, Output>(calculate: (input: Input) => Output) => BackgroundComputation<Input, Output> | Wrap one pure function as a stable definition. |
useBackgroundComputation<Input, Output>(definition: BackgroundComputation<Input, Output>, input: Input | null) => { result: BackgroundResult<Output> | null; getCurrent: () => BackgroundResult<Output> | null }Run the latest input. Null disables the owner. | <Input, Output>(definition: BackgroundComputation<Input, Output>, input: Input | null) => { result: BackgroundResult<Output> | null; getCurrent: () => BackgroundResult<Output> | null } | Run the latest input. Null disables the owner. |
createLatestComputation<Input, Output>(definition: BackgroundComputation<Input, Output>, changed: (state: BackgroundState<Output>) => void) => { update: (input: Input) => void; getCurrent: () => BackgroundResult<Output> | null; dispose: () => void }An owner outside React. Dispose is idempotent. | <Input, Output>(definition: BackgroundComputation<Input, Output>, changed: (state: BackgroundState<Output>) => void) => { update: (input: Input) => void; getCurrent: () => BackgroundResult<Output> | null; dispose: () => void } | An owner outside React. Dispose is idempotent. |
| Result | Type | Description |
|---|---|---|
revisionnumberIncreases within one owner. | number | Increases within one owner. |
valueOutputThe calculated value. | Output | The calculated value. |
The owner state is idle or computing with a revision, ready with
the result, or failed with a revision and the error.
| iOS | Android | Web | |
|---|---|---|---|
| Engine | Worklets runtime | Worklets runtime | Own worker |
| Shared | One per app | One per app | One per owner |
| Errors | To boundary | To boundary | To boundary |
| Server | No worker | No worker | Null result |
import { useMemo } from 'react'import { Text } from 'react-native'import { useBackgroundComputation } from 'one/background'import { count } from './countValues'
function FilteredCount({ values, minimum }: { values: number[]; minimum: number }) { const input = useMemo(() => ({ values, minimum }), [values, minimum]) const { result, getCurrent } = useBackgroundComputation(count, input)
function log() { console.log('accepted result:', getCurrent()) }
return ( <Text onPress={log}> {result === null ? 'counting…' : `${result.value} match`} </Text> )}The memoized input keeps identical renders from restarting the filter.
getCurrent answers taps with the accepted result without subscribing
the handler to updates.
import { createLatestComputation } from 'one/background'import { count } from './countValues'
export function startScoring( consume: (value: number) => void, report: (error: Error) => void) { const owner = createLatestComputation<{ values: number[]; minimum: number }, number>( count, (state) => { if (state.phase === 'ready') consume(state.result.value) if (state.phase === 'failed') report(state.error) } ) owner.update({ values: [1, 4, 8], minimum: 4 }) return owner}The state callback replaces the error boundary outside React: ready carries the result, failed carries the error. The caller disposes the returned owner when scoring ends, after which updates do nothing.
Edit this page on GitHub.