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

overstate

v1.2.3

Published

Infinitely composable state + actions 🎈

Downloads

79

Readme

Build Status Code Coverage downloads version MIT License

size gzip size

Why?

You want to write data models that would have worked 5 years ago and will still work in 5 years time.

What?

A data store that combines state + actions into a single model object, composed of other model objects.

By returning the state changes (or a promise with the changes) in your model functions, you can assemble powerful applications with no boilerplate, asynchronous programming, lazy loading, and type safety, all the while only depending on this library in a single file.

Table of Contents

Hello World

import { createStore } from 'overstate';

const store = createStore({
  name: 'World',
  setNameTo(aNewName) {
    return { name: aNewName };
  }
});

store.subscribe((model) => document.body.innerHTML = `Hello ${model.name}`);
store.update();

Calling store.update() initially renders "Hello World".

Calling store.model.setNameTo('πŸ˜‹') anytime renders "Hello πŸ˜‹" and so on.

Counter

import { createStore } from 'overstate';

const store = createStore({
  count: 0,
  down() {
    return { count: this.count - 1 };
  },
  up() {
    return { count: this.count + 1 };
  }
});

store.subscribe((model) => document.body.innerHTML = `Count: ${model.count}`);
store.update();

Calling store.model.down() or store.model.up() updates the count and calls the function passed to store.subscribe with the new data.

For example, adding this code will increment the counter and render it every second:

setInterval(store.model.up, 1000);

API Reference

createStore

Creates a store from a source object, deep copying all values and proxying all functions to call store.update when executed.

Can optionally receive a second argument to customize behavior:

const store = createStore(model, {
  merge(target, source, createProxyFunction) {
    // Customize the way `source` is merged into `target`.
    // Don't forget to call `createProxyFunction` on functions to make them update the state automatically!
  },
  callFunction(fn, state, args) {
    // Customize the way functions are called.
    // If you prefer not to use `this`, you can change the
    // signature of your functions to `(state, ...args) => changes`
    // or even `(state) => (...args) => changes`
  }
});
const store = createStore(model, {
  merge(target, source, createProxyFunction) {
    for (let key in source) {
      if (typeof source[key] === 'function') {
        // Proxy functions so they automatically resolve promises and update state
        target[key] = createProxyFunction(source[key], target);
      }
      else {
        target[key] = source[key]; // Yay, shallow merge! πŸŽ‰
      }
    }
    return target;
  }
});

const store = createStore({
    count: 0,
    down: (state) => ({ count: state.count - 1 }),
    up: (state) => ({ count: state.count + 1 }),
    add: (state, value) => ({ count: state.count + value })
  }, {
  callFunction: (fn, state, args) => fn(state, ...args)
});

Or if you like (state) => (...args) more:

const store = createStore({
    count: 0,
    down: (state) => () => ({ count: state.count - 1 }),
    up: (state) => () => ({ count: state.count + 1 }),
    add: (state) => (value) => ({ count: state.count + value })
  }, {
  callFunction: (fn, state, args) => fn(state)(...args)
});

Then, you can still call your functions the same way you normally would:

store.model.add(5); // store.model.count === 5
store.model.up();   // store.model.count === 6
store.model.down(); // store.model.count === 5

If you're using TypeScript, be aware that this will mess with the type definitions, because we're changing the function signature!

store.model

An object composed of all values and proxied functions passed to createStore.

To call suscriptions when proxied, model functions should return (or resolve to) an object.

store.set

Merges some data into the store model at the root level and calls store.update.

It's a built-in shortcut for this:

const store = createStore({
  set(data) {
    return data;
  }
});

In that case, store.set will do the same thing as store.model.set.

store.subscribe

Calls the passed function every time a model function that returns (or resolves to) an object is executed.

Returns an unsubscribe function that you can call to remove the subscription.

Tread lightly when rendering in subscriptions - they're not throttled or rate-limited in any way!

store.update

Calls all subscriptions manually. You usually only do this once after creating the store.

Features

Return Changes

A wise man once said:

Return the change that you wish to see in the world.

When you call a function that returns (or resolves to) an object, the data is deeply merged into the current model:

const store =
    createStore({ a: 1, b: { c: 2, d: 3 }, set: (data) => data                                  });
store.model.set({       b: {       d: 4 },                      setA: (value) => ({ a: value }) });
// New model:  ({ a: 1, b: { c: 2, d: 4 }, set: (data) => data, setA: (value) => ({ a: value }) });

In this case, set allows changing any property of the model, while setA only allows changing the a property.

Functions are proxied to automatically call store.update if they return (or resolve to) an object when executed.

So if you call store.model.setA(5), it will call store.update afterwards as well.

Asynchronous Functions

Promises are supported out of the box - store.update is called after the promise resolves:

export const CounterModel = {
  count: 0,
  async down() { // sweet async / await goodness 🍰
    const value = await Promise.resolve(-1); // Get the value from some remote server
    return { count: this.count + value });
  },
  up() { // ye olde promises πŸ§“
    return Promise.resolve(1).then((value) => ({ count: this.count + value });
  }
};

Composition

You can put objects inside objects:

export const ABCounterModel = {
  counterA: CounterModel,
  counterB: CounterModel
};

This allows you to build a hierarchical tree of data and functions.

Lazy Loading

So you want to do code splitting with webpack and have functions from the imported modules automatically call store.update data when executed?

Here are a few ways to do it:

const store = createStore();

// Get a named export
import('./counter-model').then((exports) => store.set({ counter: exports.CounterModel }));
// Get multiple named exports
import('./another-model').then((exports) => store.set({ A: exports.ModelA, B: exports.ModelB }));
// Get default export
import('./yet-another-model').then((exports) => store.set({ C: exports.default }));
// Get all exports
import('./utils-model').then((exports) => store.set({ utils: exports }));

When the import promise resolves, the model's functions proxied from CounterModel will automatically call store.update when executed.

So to lazy load data without touching the store, you can do this:

export const LazyLoadedModel = {
  set(data) {
    return data;
  },
  loadChildModels() {
    // Get a named export
    import('./counter-model').then((exports) => this.set({ counter: exports.CounterModel }));
    // Get multiple named exports
    import('./another-model').then((exports) => this.set({ A: exports.ModelA, B: exports.ModelB }));
    // Get default export
    import('./yet-another-model').then((exports) => this.set({ C: exports.default }));
    // Get all exports
    import('./utils-model').then((exports) => this.set({ utils: exports }));
  }
};

Then define your store and load the models:

const store = createStore({ lazy: LazyLoadedModel });
store.model.lazy.loadChildModels();

The child models will be inserted into the model's data when the import is done.

TypeScript

Overstate is written in TypeScript, so if you use it you get autocomplete and type checking out of the box.

Going back to the Counter example:

store.model.up(5); // [ts] Expected 0 arguments, but got 1.

However, this doesn't get type definitions inside objects:

export const CounterModel = {
  count: 0,
  add(value: number) {
    return { count: this.count + value }; // Hmm, `this` is of type `any` here πŸ˜•
  }
};

And we can't do add(this: typeof CounterModel, value: number) either, because we're referencing an object inside its own definition.

So...read on.

Classes

To get type safety inside your models, or if you just prefer to, you can use classes instead of objects:

export class CounterModel {
  count = 0;
  add(value: number) { // or `add(value) {` if you don't use TypeScript
    return { count: this.count + value }; // Yay, `this` is of type `CounterModel` πŸ˜„
  }
};

And then when creating your store:

const store = createStore(new CounterModel());
store.model.add('1'); // [ts] Argument of type '"1"' is not assignable to parameter of type 'number'.

Arrow Functions

Be careful with those if you're using this inside your model functions - as expected, it would refer to the parent context. Because functions are proxied when the store is created, class methods defined as arrow functions won't refer to the correct this either.

Debugging

First, make sure you have the Redux devtools extension for your browser. Then:

import { createStore } from 'overstate';
import { debug } from 'overstate/debug/redux-devtools';
import { CounterModel } from './counter-model';

const store = process.env.NODE_ENV === 'production' ? createStore(CounterModel) : debug(createStore(CounterModel));

Rendering

For examples with different view layers, see the CodePen collection.

Here's a counter example with picodom:

/** @jsx h */
import { createStore } from 'overstate';
import { app } from 'overstate/app/picodom';
import { h, patch as render } from 'picodom';
import { CounterModel } from './counter-model';

const Counter = ({ model }) =>
  <div>
    Your count is: {model.count}
    <button onclick={model.down}>-</button>
    <button onclick={model.up}>+</button>
  </div>
;

app({
  store: createStore(CounterModel),
  view: Counter,
  render
});

All functions in the model are bound to the correct context, so you can write onclick={model.up} instead of onclick={() => model.up()}.

The app function is a very thin layer on top of Overstate to reduce boilerplate.

You can pass a custom DOM element to render into as the second argument, which is document.body by default.

It also returns an unsubscribe function to stop rendering, effectively "destroying" your app, although the store will still work just fine.

app uses requestAnimationFrame by default to throttle rendering. Alternatively, provide your own function in app({ throttle: ... }).

FAQs

this is bad and you should feel bad πŸ¦€

Hey, that's not a question! Anyway, if you prefer state or something else instead of this, you can use the callFunction option when creating a store, as described in the custom function calls section.

So this is cool, where can I find out more?

I'm glad you asked! Here are some useful resources: