Skip to main content
Version: Unreleased

registerCustomSerializable

Available from 0.7.0

registerCustomSerializable lets you register your own pre-serialization and post-deserialization logic. This is necessary for objects with prototypes different than just Object.prototype or some other built-in prototypes like Map etc. The Worklets library can't handle such objects by default to convert into Serializables hence you need to register them as Custom Serializables. This way you can tell the Worklets library how to transfer your custom data structures between different Runtimes without manually serializing and deserializing them every time.

List of supported types for Serialization can be found here.

Reference​

This example transfers a Point with a custom prototype to a Worker Runtime. Its x and y coordinates are ordinary data, but its length() method belongs to the prototype.

Register the serializer on the RN Runtime before passing the point to another runtime. scheduleOnRuntime then handles the transfer automatically:

  1. determine recognizes the point on RN.
  2. pack runs on RN and returns just { x, y }.
  3. unpack runs on the worker and creates a new point with its own prototype and length() method.

Code

Code

Colours connect each executing thread to its current code line. Recently executed lines fade over the next three ticks.

function createPoint(x, y) {
'worklet';
const prototype = {
kind: 'Point',
length() {
return Math.hypot(this.x, this.y);
},
};
return Object.assign(Object.create(prototype), { x, y });
}
function showPoint(point) {
'worklet';
console.log('Distance:', point.length()); // 5
}
registerCustomSerializable({
name: 'Point',
determine(value) {
'worklet';
const matches = Object.getPrototypeOf(value)?.kind === 'Point';
console.log('determine:', matches);
return matches;
},
pack(point) {
'worklet';
console.log('pack:', point.x, point.y);
return { x: point.x, y: point.y };
},
unpack(data) {
'worklet';
console.log('unpack:', data.x, data.y);
return createPoint(data.x, data.y);
},
});
const worker = createWorkletRuntime({ name: 'background' });
const point = createPoint(3, 4);
scheduleOnRuntime(worker, showPoint, point);
What crosses the runtime boundary?

The packed coordinates cross the boundary. The original object and its prototype stay on RN. unpack uses createPoint to reconstruct the object on the worker, where point.length() returns 5. The console identifies the runtime running each callback. You don't call pack or unpack yourself.

Type definitions

function registerCustomSerializable<
TValue extends object,
TPacked extends object,
>(registrationData: RegistrationData<TValue, TPacked>): void;

type RegistrationData<TValue extends object, TPacked = unknown> = {
name: string;
determine: (value: object) => value is TValue;
pack: (value: TValue) => TPacked;
unpack: (value: TPacked) => TValue;
};

Arguments​

registrationData​

An object containing the registration data for the Custom Serializable.

Available properties:

NameTypeDescription
namestringA unique name for the Custom Serializable. It's used to prevent duplicate registrations of the same Custom Serializable. You will get warned if you attempt to register a Custom Serializable with a name that has already been used.
determine(value: object) => value is TValueA worklet function that checks whether a given JavaScript value is of the type handled by this Custom Serializable.
pack(value: TValue) => TPackedA worklet function that packs the JavaScript value of type TValue into a value that can be serialized by default as Serializable. The function must return a supported type for Serialization.
unpack(value: TPacked) => TValueA worklet function that unpacks the packed value, after it's been deserialized from its packed form, back into the JavaScript value of type TValue.

Motivation​

Custom prototypes are bound to a single Runtime and don't exist in other Runtimes. Due to that it's impossible to transfer them between runtimes directly. This is why a pre-serialization (packing) and pre-deserialization (unpacking) logic is required.

Consider the following examples:

const obj = {};

// because why not
Object.setPrototypeOf(obj, console);

scheduleOnUI(() => {
// This will throw because `obj`
// had a custom prototype and
// it couldn't be serialized.
obj.log('Hello!');
});
const obj = {};

// because why not
Object.setPrototypeOf(obj, console);

type ConsoleLike = typeof console;

registerCustomSerializable({
name: 'ConsoleLike',
determine(value: object): value is ConsoleLike {
'worklet';
return Object.getPrototypeOf(value) === console;
},
pack(value: ConsoleLike) {
'worklet';
// We don't need to transfer any data,
// so we can pack it to an empty object.
return {};
},
unpack(value: object) {
'worklet';
// We can recreate the original object.
return Object.create(console);
},
});

scheduleOnUI(() => {
obj.log('Hello!'); // prints 'Hello!'
});

Remarks​

  • To use Custom Serializables which require new keyword for instantiation, you need to disable Worklet Classes option in Worklets Babel plugin configuration.
  • Custom Serializables are global and shared between all Worklet Runtimes. Once you register a Custom Serializable, it will be available in all Runtimes.
  • You can use registerCustomSerializable only on the RN Runtime.