Skip to content

zuze-lab/stateful

Repository files navigation

@zuze/stateful

npm version Coverage Status Build Status Bundle Phobia

What is this?

It's a ridiculously tiny and highly performant state mangement solution when you don't want to implement redux. It's supposed to be minimalistic (comes in most handy for a source of state for library developers) and extremely simple. It comes with everything you need (including a super tiny memoized selector implementation) to maintain your state.

Getting Started

Install it as a dependency in your JavaScript/Typescript project

npm install @zuze/stateful
# or
yarn add @zuze/stateful

Or just pull it in from the browser:

<script src="https://unpkg.com/@zuze/stateful"></script>
<script>
  const { state } = stateful;
  const myState = state('jim!');
  myState.subscribe(console.log); // jim!
</script>

API

state(initialState: T): Stateful<T>

Create a stateful instance with an initial state. Returns the stateful interface:

  • getState(): T Returns the current state.

  • setState((state: T) => T): void Can be used set state using a function

    import { state } from '@zuze/stateful';
    
    const s = state({ fetching: false, error: false });
    s.setState(state => ({ ...state, fetching: false, data: 'some data' })); // { fetching: false, error: false, data: 'some data' }
    import { state } from '@zuze/stateful';
    
    const s = state({ fetching: false, error: false });
    s.setState(state => ({ ...state, fetching: false, data: 'some data' })); // { fetching: false, error: false, data: 'some data' }
  • subscribe(subscriberFunction: Subscriber<T>): Unsubscribe Register a subscriber function to be notified every time the state changes (see selectors). Returns an unsubscribe function.

    const s = state('jim');
    const unsub = s.subscribe(console.log); // logs jim
    s.setState(() => 'fred'); // logs fred
    unsub();
    s.setState(() => 'bill'); // nothing logged

createSelector(...selectors, combiner)

The purpose of a selector (popularized in reselect) is to minimize expensive computations through memoization.

There is an alternate method for using selectors outside of minimizing expensive computations: because the combiner function only gets called when at least one of it's arguments change, it essentially becomes a callback for changes in the input selectors.

import { createSelector, state } from '@zuze/stateful';

const myFetchingSelector = createSelector(
    ({ fetching }) => fetching,
    (fetching) => {
        console.log("fetching changed",fetching);
    }
);

const s = state({
    fetching: false,
    data: null,
    error: true;
});

s.subscribe(myFetchingSelector); // logs "fetching changed",false

(async() => {selectors
    s.setState(state => ({ ...state, fetching: true })); // logs "fetching changed",true

    try {
        const data = await someAPICall();
        s.setState(state => ({ ...state, data })) // not called!
    } catch {
        s.setState((state => ({ ...state, error: true })) // not called!
    }

    s.setState((state => ({ ...state, fetching: false })); // logs "fetching changed",false
});

memo(fn)

Bare bones memoization implementation. You aren't allowed to mess with the comparator

import { memo } from '@zuze/stateful';

const myFunc = (...someArgs) => {
  // ... some expensive computations
  return 42;
};

const memoed = memo(myFunc);
console.log(memoed(...someArgs)); // outputs 42 - expensive computations performed
console.log(memoed(...someArgs)); // outputs 42 - expensive computations skipped!

About Selector Memoization

There are 2 levels of memoizations going on when creating a selector.

  1. The function returned from createSelector itself is memoized using checker
  2. The combiner function is memoized using checker.

What this effectively means is:

  1. If the function returns from createSelector is called with the same arguments neither the input selectors nor the combiner will be called
  2. If the function returned from createSelector is called with different arguments, all input selectors will be called. If these executions result in the same arguments as the last time, the combiner will NOT be called.