final-state

Build Status codecov.io Known Vulnerabilities minified + gzip styled with prettier

final-state

A lightweight, framework agnostic state management.

Installation

# Install peer dependencies
yarn add immer
# Install final-state
yarn add final-state

final-state is written in Typescript, so you don’t need to find a type definition for it.

Basic Example

import { createStore } from 'final-state';

// Define initial state
const initialState = {
  a: 1,
  b: 'good',
};

// Define actions
const actions = {
  increaseA(draftState, n = 1) {
    draftState.a += n;
  },
};

// Create store instance
const store = createStore(initialState, actions, 'store-name');

// Print state
console.log('INITIAL STATE:', store.getState());

// Define a listener to listen the changes of state
function listener() {
  // Print state
  console.log('IN SUBSCRIBE LISTENER:', store.getState());
}

// Subscribe the changes of state
store.subscribe(listener);

// Dispatch action
store.dispatch('increaseA');

// Print state
console.log('CURRENT STATE:', store.getState());

store.unSubscribe(listener);

/* Output will be:
INITIAL STATE: Object {a: 1, b: "good"}
IN SUBSCRIBE LISTENER: Object {a: 2, b: "good"}
CURRENT STATE: Object {a: 2, b: "good"}
*/

API Reference

createStore(initialState, actions[, name])

Create a store instance. You can create multiple stores in your app.

Parameters:

Store#getState()

Get the latest state object. Keep in mind that you shouldn’t mutate the state object directly. It will not take effect until next Store#dispatch and may cause your app broken.

Store#subscribe(listener)

Subscribe the changes of state. Once the state are changed by Store#dispatch, the listener will be called.

It returns a function to let you unsubscribe this listener:

const unSubscribe = store.subscribe(listener);
unSubscribe();

listener

The listener is a function with the following signature:

/**
 * Listener type
 *
 * @template T the type of your state
 */
export type Listener<T = any> = (type?: string, prevState?: T) => void;

A basic example of using type and prevState:

// final-state-logger
store.subscribe((type, prevState) =>
  console.log(type, prevState, store.getState()),
);

Store#unSubscribe(listener)

Unsubscribe a listener. The listener should exactly be same with the one passed to Store#subscribe.

Store#dispatch(action[, params]) [overload]

// definition
/**
 * An overload of dispatch
 *
 * Dispatch an action that has been defined and named.
 * @param {string} action the name of action
 * @param {K} params the type of your action parameters
 * @template K the type of your action parameters
 * @returns a promise to indicate the action is totally finished
 */
public dispatch<K = undefined>(action: string, params?: K): Promise<void>;

Dispatch an action to alter state.

The first parameter action is the name of the action function which will be triggered.

The second parameter params is the dynamic values that are used by action function.

It returns a Promise to indicate whether the action is totally finished.

store.dispatch(...).then(() => {
  // action is totally finished
});

⚡️️️️️️️Important Notes!!!

When you dispatch an async action like this:

const actions = {
  async someAsyncAction(draftState) {...},
};
// ...
store.dispatch('someAsyncAction');
// store.getState() is still old state

store.dispatch('someAsyncAction').then(() => {
  // store.getState() is the latest state
});

You can’t get the latest state right after dispatching. Because as it’s name says, it is asynchronous.

Store#dispatch(action[, params]) [overload]

// definition
/**
 * An overload of dispatch
 *
 * Dispatch an action that is defined temporarily
 * @param {Action<T, K>} action the action function
 * @param {K} params the type of your action parameters
 * @template K the type of your action parameters
 * @returns a promise to indicate the action is totally finished
 */
public dispatch<K = undefined>(
  action: Action<T, K>,
  params?: K,
): Promise<void>;

Dispatch an action to alter state.

The first parameter action is the action function which will be triggered.

The second parameter params is the dynamic values that are used by action function.

It returns a Promise to indicate whether the action is totally finished.

Async action function is also supported.

Store#registerActionHandler(name, handler)

Anyone can write his own action handler to handle the custom actions.

The first parameter name is the name of your handler.

The second parameter handler is a function with the following signature:

/**
 * Type of plugin handler to handle actions
 * @param {PluginAction} pluginAction the action object of plugins
 * @param {any} params the parameters of action
 */
export type ActionHandler = (pluginAction: PluginAction, params?: any) => void;

Let’s see a simple example:

// Register a custom handler that can handle observable actions
import { Observable } from 'rxjs';
import { createStore } from 'final-state';

const initialState = {
  a: 0,
};

const actions = {
  increaseA(draftState, n = 1) {
    draftState.a += n;
  },
  rxIncreaseA: {
    handler: 'rx',
    action(n = 1) {
      return new Observable(subscriber => {
        subscriber.next(['increaseA', n]);
        setTimeout(() => {
          subscriber.next('increaseA');
          subscriber.complete();
        }, 200);
      });
    },
  },
};

const store = createStore(initialState, actions, 'custom-handler-example-store');

store.registerActionHandler('rx', (pluginAction, params) => {
  return new Promise((resolve, reject) => {
    pluginAction.action(params).subscribe({
      next(value) {
        if (Array.isArray(value)) {
          store.dispatch(...value);
        } else if (typeof value === 'string') {
          store.dispatch(value);
        }
      },
      error: reject,
      complete() {
        resolve();
      },
    });
  });
});

store.dispatch('rxIncreaseA', 5);

// a = 5 now
// after 1000 milliseconds, a = 6

Use with typescript

import { createStore, Action, ActionMap } from 'final-state';

// Define state shape
interface State {
  a: number;
  b: string;
}

// Define initial state
const initialState: State = {
  a: 1,
  b: 'good',
};

// Define actions
const actions: ActionMap<State> = {
  increaseA(draftState, n = 1) {
    draftState.a += n;
  },
};

// Create store instance
const store = createStore<State>(initialState, actions);

// Print state
console.log('INITIAL STATE:', store.getState());

// Define a listener to listen the changes of state
function listener() {
  // Print state
  console.log('IN SUBSCRIBE LISTENER:', store.getState());
}

// Subscribe the changes of state
store.subscribe(listener);

// Dispatch action
store.dispatch('increaseA');

// Print state
console.log('CURRENT STATE:', store.getState());

store.unSubscribe(listener);

/* Output will be:
INITIAL STATE: Object {a: 1, b: "good"}
IN SUBSCRIBE LISTENER: Object {a: 2, b: "good"}
CURRENT STATE: Object {a: 2, b: "good"}
*/

Test

This project uses jest to perform testing.

yarn test