One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Background Computation

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.

Usage

// calculate.ts: synchronous pure JavaScript, including any imported helpers
export function countValues(input: { values: number[]; minimum: number }) {
return input.values.filter((value) => value >= input.minimum).length
}
countValues.ts
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 }.

How it works

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.

API reference

FunctionsTypeDescription
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.
ResultTypeDescription
revisionnumberIncreases within one owner.
numberIncreases within one owner.
valueOutputThe calculated value.
OutputThe calculated value.

The owner state is idle or computing with a revision, ready with the result, or failed with a revision and the error.

Platform differences

iOSAndroidWeb
EngineWorklets runtimeWorklets runtimeOwn worker
SharedOne per appOne per appOne per owner
ErrorsTo boundaryTo boundaryTo boundary
ServerNo workerNo workerNull result

Limits and gotchas

Examples

Filter a large list off-thread

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.

Score moves outside React

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.