Skip to content

mfellner/valtio-factory

Repository files navigation

valtio-factory

Build Status Codecov Build Size Version Downloads

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 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.

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 });

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() {
      // Derived properties are available in subsequently declared actions.
      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(/* context */ undefined, { count: 1 });

Subscribe

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();

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 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);
  });
});

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, {
  // 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();

Access the parent store

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,
});

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() {
  // 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>
  );
}

Example

You can find an example with React and valtio-factory in this repository at example/ or on Codesandbox.