valtio-factory 🏭
Create valtio state using the factory pattern
A proxy object is created from initial state.
Existing valtio functions can be used normally.
import { createFactory } from '@mfellner/valtio-factory';
import { subscribe } from 'valtio';
const state = createFactory({ count: 0 }).create();
state.increment();
subscribe(state, () => console.log('state:', state));
Motivation
Valtio already offers several simple recipes for organizing actions, persisting state, and composing states.
This library provides a comprehensive and opinionated solution on top valtio for creating "stores" (state + actions) using the factory pattern. Specifically, it simplifies the following things:
- Separation of store declaration and initialization
- Initializing stores from external data sources (e.g. local storage, async storage)
- Declaring actions and binding them to state
- Declaring derived state and subscriptions
- Injecting dependencies into actions and other state-dependent logic with context
- Composing multiple stores
valtio-factory was partially inspired by MobX-State-Tree.
Define actions
Actions become methods on the state itself.
This is equivalent to manually declaring actions as properties of the proxy object.
const state = createFactory({ count: 0 })
.actions({
increment() {
this.count += 1;
},
})
.create();
state.increment();
Use context
A context object can be provided to actions and will be available as the property this.$context
.
The context object will be part of the state as a transitive ref
property.
Context can be used to provide external dependencies to actions, e.g. API client instances.
type State = {
count: number;
};
type Context = {
shouldIncrement: boolean;
};
const state = createFactory<State, Context>({ count: 0 })
.actions({
increment() {
if (this.$context.shouldIncrement) state.count += 1;
},
})
.create({ shouldIncrement: true });
Actions with return types
Actions may also return a value to the caller:
const state = createFactory({ count: 0 })
.actions({
increment(): number {
this.count += 1;
return this.count;
},
})
.create();
const n: number = state.increment();
Actions calling other actions
Actions can call previously defined actions.
Note that it's necessary to call the actions
factory function twice and only the the second action can call the first action, not the other way around.
const state = createFactory({ count: 0 })
.actions({
increment(n: number) {
this.count += n;
},
})
.actions({
double() {
this.increment(this.count);
},
})
.create();
Derive properties
The derived
factory function is a convenient wrapper around the derive
utility.
const state = createFactory({ count: 0 })
.derived({
doubled(state) {
return state.count * 2;
},
})
.actions({
double() {
state.count = state.doubled;
},
})
.create();
Provide initial state on initialization
The second argument of the create
method is used to initialise the proxy and to overwrite the initial state. It's a partial object that can have some but doesn't need all of the state properties.
const state = createFactory({ count: 0, bool: true }).create( undefined, { count: 1 });
Subscribe
It's possible to define subscriptions on the whole state using the factory pattern.
The subscription callback function receives the state object as a first argument, then the
factory's context, and the rest of valtio's arguments for the subscribe callback last.
const state = createFactory({ count: 0 })
.subscribe((state, context) => {
console.log('current state:', state);
})
.create();
Subscribe to snapshots
To conveniently subscribe to a snapshot of the state, use subscribeSnapshot
.
createFactory({ count: 0 }).subscribeSnapshot((snap, context) => {
});
Use onCreate
to subscribe only to portions of the state
You can use the onCreate
method to declare a callback that will receive the proxy state object when it is created by the factory.
That way you can use all of valtio's utilities like subscribe
and subscribeKey
as you normally would.
onCreate
may optionally return an unsubscribe callback function.
import { subscribeKey } from 'valtio/utils';
createFactory({ count: 0 }).onCreate((state) => {
return subscribeKey(state, 'count', (n) => {
console.log('current count:', n);
});
});
Unsubscribe
The store exposes the function $unsubscribe()
which will unsubscribe all subscriptions added to the factory.
It wil also call the unsubscribe callback returned by the onCreate
function.
const state = createFactory({ count: 0 })
.subscribe((state) => {})
.onCreate((state) => {
return subscribeKey(state, 'count', (n) => {});
})
.create();
state.$unsubscribe();
Compose factories
You can compose factories in order to create a proxy object of nested states.
const foo = createFactory({ x: 0 }).actions({
inc() {
this.x += 1;
},
});
const bar = createFactory({ y: 0 }).actions({
dec() {
this.y -= 1;
},
});
const root = createFactory({
foo,
bar,
});
const state = root.create(context, {
bar: {
y: 1,
},
});
state.foo.inc();
Access the parent store
When composing factories and their resultant state, the parent store can be accessed with the $getParent()
method inside actions.
Note that it's currently not possible to anticpate the type of the parent store and wether it will be defined.
Hence it's necessary to supply a type parameter to the $getParent function and to use optional chaining.
import { createFactory, Store } from '@mfellner/valtio-factory';
const foo = createFactory({ x: 0 }).actions({
inc() {
this.x += 1;
},
});
const bar = createFactory({ y: 0 }).actions({
dec() {
this.y -= this.$getParent?.<RootStore>()?.foo.x ?? 0;
},
});
type FooFactory = typeof foo;
type BarFactory = typeof bar;
type RootState = {
foo: FooFactory;
bar: BarFactory;
};
type RootStore = Store<typeof root>;
const root = createFactory<RootState>({
foo,
bar,
});
TypeScript
Get the result type of a factory
For convenience, you can get the result type of a factory (i.e. the type of the proxy state) with the Store
helper.
import { createFactory, Store } from '@mfellner/valtio-factory';
const counterFactory = createFactory({ x: 0 }).actions({
inc() {
this.x += 1;
},
});
type Counter = Store<typeof counterFactory>;
const counter: Counter = counter.create();
Declare a state type
Using TypeScript type arguments, you can declare optional properties or properties with union types.
type State = {
count?: number;
text: string | null;
};
const state = createFactory<State>({ text: null }).create();
Use with React
Of course you can use valtio-factory with React.
const counter = createFactory({ x: 0 })
.actions({
inc() {
this.x += 1;
},
})
.create();
function Counter() {
const state = useSnapshot(counter);
const onInc = () => counter.inc();
return (
<div>
<p>count: {state.x}</p>
<button onClick={onInc}>increment</button>
</div>
);
}
Example
You can find an example with React and valtio-factory in this repository at example/ or on Codesandbox.