Writing your own plugin

A plugin is effectively a set of callbacks and unique symbol ID. Here is an example of a plugin which has got all possible callbacks and prints console logs when called:

// should be global variable
const MyStateWatchPluginId = Symbol('MyStateWatchPlugin');
function MyStateWatchPlugin() {
return ({
id: MyStateWatchPluginId,
init: (s: State<StateValueAtRoot>) => {
console.log('plugin attached')
return ({
onSet: (data) => {
console.log('new state set', data.state);
console.log('at path', data.path);
console.log('to a new value', data.value);
console.log('from old value', data.previous);
console.log('merged with', data.merged);
},
onDestroy: (data) => {
console.log('state detroyed', data.state);
},
onBatchStart: (data) => {
console.log('batch started', data.state);
console.log('at path', data.path);
console.log('with context', data.context);
},
onBatchFinish: (data) => {
console.log('batch finished', data.state);
console.log('with context', data.context);
}
})
}
})
};

Now it can be attached to a state:

const state = createState(...);
state.attach(MyStateWatchPlugin)

When the methods from StateMethods are invoked for a state, the plugin's callbacks will be called. Learn more about the plugin interfaces in the API reference:

Note: There is no distinction between attaching a plugin to the root of the state or to any of its child. This means that in the following example, calling state.x.y.set(...) will trigger the onSet callback of the plugin, even if it has been attached to state.a.b. If your plugin requires to react only on certain sub-state updates, you can use path field from the callback arguments to filter out events of no interest.

const state = createState(...);
// behaves exactly the same as state.attach(MyStateWatchPlugin)
state.a.b.attach(MyStateWatchPlugin)

A plugin may provide additional extension methods, like the Initial plugin, for example. The best place for extension methods is alongside with the callback functions:

function MyStateWatchPlugin() {
return ({
id: MyStateWatchPluginId,
init: (s: State<StateValueAtRoot>) => {
return ({
// standard callback
onSet: (data) => { ... },
// extension method
doSomething: () => { ... }
})
}
})
};

A plugin instance can be retrieved from a state using StateMethods.attach method called by plugin ID:

const [plugin, controls] = state.attach(MyStateWatchPluginId)

The controls variable is a set of extended control methods, which allow to update the state without triggering rerendering and to rerender when a state has not been updated. This is used by the Untracked plugin, for example.

An instance of plugin will be an Error if a plugin with the specified ID has not been attached to a state. If a plugin can not function without being attached, it may just throw this error.

If it has been attached it will be the result returned by the init function defined by a plugin (see above). It means it is necessary to do the following in order to call the extension method:

(plugin as { doSomething: () => void }).doSomething()

This is usually wrapped by an overloaded plugin function itself, so it can be used like the following:

MyStateWatchPlugin(state).doSomething()

Check out how the Initial plugin does it, for example.

In case of any issues, just raise a ticket on Github.