- Create valtio state using the factory pattern
- Define actions
- Derive properties
- Provide initial state on initialization
- Subscribe
- Compose factories
- TypeScript
- Use with React
- Example
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));
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 state declaration and initialization
- Initializing state from external data sources (e.g. local storage, async storage)
- Declaring actions and binding them to state
- Declaring state subscriptions
- Injecting dependencies into actions and other state-dependent logic with context
valtio-factory was partially inspired by MobX-State-Tree.
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();
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 });
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() {
// Derived properties are available in subsequently declared actions.
state.count = state.doubled;
},
})
.create();
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(/* context */ undefined, { count: 1 });
It's possible to define subscriptions on the whole state using the factory pattern.
const state = createFactory({ count: 0 })
.subscribe((state) => {
console.log('current state:', state);
})
.create();
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 valtio's subscribe
and subscribeKey
as you normally would.
import { subscribeKey } from 'valtio/utils';
createFactory({ count: 0 }).onCreate((state) => {
subscribeKey(state, 'count', (n) => {
console.log('current count:', n);
});
});
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, {
// The initial state object will use the keys of the factory properties.
bar: {
y: 1,
},
});
// The resulting proxy object will have properties with the individual state objects.
state.foo.inc();
When composing factories and their resultant state, the parent store can be accessed with the $getParent()
method inside actions.
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,
});
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();
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();
Of course you can use valtio-factory with React.
const counter = createFactory({ x: 0 })
.actions({
inc() {
this.x += 1;
},
})
.create();
function Counter() {
// Call `useSnapshot` on the store (i.e. proxy state object).
const state = useSnapshot(counter);
// Use action methods directly from the store, not from the state snapshot!
const onInc = () => counter.inc();
return (
<div>
<p>count: {state.x}</p>
<button onClick={onInc}>increment</button>
</div>
);
}
You can find an example with React and valtio-factory in this repository at example/ or on Codesandbox.