Custom Plugins
Creating custom plugins for Store
Custom Plugins
Create your own plugins to extend store functionality.
Plugin Interface
Plugins are functions that receive the store API and return a plugin result.
import type { Plugin } from "@jlnstack/store/plugins"
const myPlugin = ((store) => ({
id: "my-plugin",
middleware: (setState, getState) => (updater) => {
setState(updater)
},
onStateChange: (state, prevState) => {},
extend: {
hello: () => "world",
},
})) satisfies PluginPlugin Hooks
id
Required unique identifier. Extensions are accessed via extension[id].
middleware
Wraps setState. Receives (setState, getState) and returns a new setState.
Useful for logging, immer-style updates, or other behaviors.
import type { Plugin } from "@jlnstack/store/plugins"
const debug = ((_) => ({
id: "debug",
middleware: (setState, getState) => (updater) => {
const prev = getState()
setState(updater)
const next = getState()
console.log({ prev, next })
},
})) satisfies PluginonStateChange
Called after setState runs (not triggered by setStateSilent).
import type { Plugin } from "@jlnstack/store/plugins"
const analytics = ((_) => ({
id: "analytics",
onStateChange: (state, prevState) => {
trackStateChange(state, prevState)
},
})) satisfies Pluginextend
Adds methods or values accessible via extension[id] (or useExtensions() in React).
extend is an object, not a function.
import type { Plugin } from "@jlnstack/store/plugins"
const timestamps = ((store) => ({
id: "timestamps",
extend: {
now: () => Date.now(),
getState: store.getState,
},
})) satisfies PluginExample: Persistence Plugin
import { createStore } from "@jlnstack/store"
import { plugins } from "@jlnstack/store/plugins"
import type { Plugin } from "@jlnstack/store/plugins"
function persist(key: string) {
return ((store) => ({
id: "persist",
extend: {
save: () => {
localStorage.setItem(key, JSON.stringify(store.getState()))
},
load: () => {
const saved = localStorage.getItem(key)
if (saved) store.setState(JSON.parse(saved))
},
clear: () => {
localStorage.removeItem(key)
},
},
})) satisfies Plugin
}
const { extension } = createStore({
state: { count: 0 },
actions: () => ({}),
plugins: plugins([persist("my-store")]),
})
extension.persist.save()
extension.persist.load()
extension.persist.clear()Example: Debounce Plugin
import { createStore } from "@jlnstack/store"
import { plugins } from "@jlnstack/store/plugins"
import type { Plugin } from "@jlnstack/store/plugins"
function debounce(callback: () => void, ms: number) {
return ((_) => {
let timeout: ReturnType<typeof setTimeout>
return {
id: "debounce",
onStateChange: () => {
clearTimeout(timeout)
timeout = setTimeout(callback, ms)
},
}
}) satisfies Plugin
}
const { store } = createStore({
state: { query: "" },
actions: () => ({}),
plugins: plugins([debounce(() => console.log("Debounced!"), 300)]),
})
store.setState((s) => ({ ...s, query: "hello" }))Type Safety
Extensions are fully typed based on the extend object:
import { createStore } from "@jlnstack/store"
import { plugins } from "@jlnstack/store/plugins"
import type { Plugin } from "@jlnstack/store/plugins"
const typed = ((_) => ({
id: "typed",
extend: {
foo: () => "bar",
count: 42,
},
})) satisfies Plugin
const { extension } = createStore({
state: {},
actions: () => ({}),
plugins: plugins([typed]),
})
extension.typed.foo() // string
extension.typed.count // number