@artizon/store
v0.2.3
Published
A zustand-like state management solution.
Downloads
5
Maintainers
Readme
store
A zustand-like state management solution.
Comparision with Zustand:
- Safer. States are runtime immutable as they are freezed with
Object.freeze
. - Support any state types. Zustand assumes state is an object.
- Addition of
DeriveStore
which creates a store that derives its state from any number of other stores. - Middlewares are implemented using JS proxy.
- No cryptic TS typings in source code.
Usage
npm i @artizon/store
ObjectStore
(similar to Zustand-store)
const counterStore = createObjectStore<CounterState>((set) => ({
count: 0,
inc: () => set((state) => ({ count: state.count + 1 })),
}));
// Argument to `create*Store` function can be a plain object too
const anotherStore = createObjectStore<{ x: number }>({ x: 3 });
function Counter() {
const count = useStore(counterStore, (s) => s.count);
return <div>count: {count}</div>;
}
SimpleStore
(similar to Jotai atom)
const store = createSimpleStore("Hello");
store.setState("Hello");
DeriveStore
(similar to Jotai derive atom, but work with stores too)
const depStore1 = createObjectStore({
value: [1],
});
const depStore2 = createObjecStore({
value: [2],
});
// Working on removing the need to specify types of stores as the second type arg
const deriveStore = derive<string, [typeof depStore1, typeof depStore2]>(
[depStore1, depStore2],
([dep1, dep2], prevDeps, prevState) => {
return `${dep1} ${dep2}`;
}
);
Middleware
const store = subscribeWithSelector(
createObjectStore({
x: [1],
y: "testing",
})
);
// Select a slice (a property of the state)
store.subscribe(
(state, prevState) => {
// ...
},
(state) => state.y,
false // partial?
);
// Select a partial (subset of the state)
store.subscribe(
(state, prevState) => {
// ...
},
(state) => {
y: state.y;
},
true // partial?
);
// Can be used as normal `subscribe` too
store.subscribe((state, prevState) => {
// ...
});
Middleware System - Design Decision
It seems like the majority of the complexity of Zustand comes from its middleware system. The challenge of creating such a middleware system is that the Store
(i.e. StoreApi
in Zustand terms) interface changes dynamically as middlewares are added to the store. For instance, when the subscribeWithSelector
middleware is added to the store, the subscribe
function takes a "selector" as an additional argument. In order to get type support for this behaviour, a lot of TS tricks are used.
This library's middleware system is facilited by JS Proxy. Each middleware must specify what Store
interface it can accept as the input. For instance, the subscribeWithSelector
middleware requires the input store to extends:
export type ObjectStoreSubscribeListener<T> = (state: T, prevState: T) => void;
export type ObjectStoreUnsubscriber = () => void;
interface Store {
readonly setState: AnyFunction;
readonly getState: AnyFunction;
readonly subscribe: (
listener: ObjectStoreSubscribeListener<T>
) => ObjectStoreUnsubscriber;
}
This is because the subscribeWithSelector
middleware will "hijack" the subscribe
function of the input store and hence it expects it to be in a particular form.
Middleware must also specify the type of the store after its mutation. For instance, the output type of the subscribeWithSelector
middleware will be:
/**
* A type function that overwrites the properties of `T` with the properties of `U`.
*/
type Overwrite<T, U> = Omit<T, keyof U> & U;
type OutputStore = Overwrite<
InputStore,
{
/**
* A subscribe function with a selector function and a `partial` boolean field as additional arguments.
* @param listener A function that will be called when the subscribed state changes.
* @param selector A function that selects a slice or a partial of the state to subscribe to.
* @param partial A boolean field that indicates whether the selector function selects a partial of the state or a slice of the state.
* The field is used to determine how to equality checks should be performed, which in turn governs when the `listener` function will be triggered.
* - If `partial` is `false` or `undefined`, `Object.is` will be used to check for equality between states.
* - If `partial` is `true`, a 1-layer-deep `Object.is` will be used to check for equality between states.
* @returns void
*/
subscribe: CustomSubscribeFunction;
}
TODO
- More tests e.g. with SSR
MutableStore
(similar to Valtio)