npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@mfellner/valtio-factory

v1.0.2

Published

Create and compose valtio state using the factory pattern.

Downloads

25

Readme

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

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) => {
  // `snap` is an immutable object
});

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) => {
    // The function returned by onCreate will be called when $unsubscribe() is called.
    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, {
  // 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.

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