Giter Site home page Giter Site logo

icestore's Introduction

English | 简体中文

icestore

Lightweight React state management library based on react hooks

NPM version Package Quality build status Test coverage NPM downloads Known Vulnerabilities David deps

Installation

icestore requires React 16.8.0 or later.

npm install @ice/store --save

Introduction

icestore is a lightweight React state management library based on hooks. It has the following core features:

  • Minimal & Familiar API: No additional learning costs, easy to get started with the knowledge of React Hooks.
  • Enough & Extensible: Cover 80% most common usage scenes and extend the remaining 20% scenes with builtin middleware mechanism.
  • Class Component Support: Make old projects enjoying the fun of lightweight state management with friendly compatibility strategy.
  • Built in Async Status: Records loading and error status of async actions, simplifying the rendering logic in the view layer.
  • Typescript Support: Provide complete type definitions to support intelliSense in VSCode.
  • Optimal Performance: Decreases the number of view components that rerender when the state changes by creating multiple stores.

The data flow is as follows:

Getting Started

Let's build a simple todo app from scatch using icestore which includes following steps:

  • Define a store config (a plain JavaScript object) which consists of function properties (correspond to the action) and other properties (correspond to state).

    // src/stores/todos.js
    export default {
      dataSource: [],
      async refresh() {
        this.dataSource = await new Promise(resolve =>
          setTimeout(() => {
            resolve([
              { name: 'react' },
              { name: 'vue', done: true },
              { name: 'angular' }
            ]);
          }, 1000)
        );  },
      add(todo) {
        this.dataSource.push(todo);
      },
    };
  • Initialize the store instance and register the pre-defined store config using the namespace.

    // src/stores/index.js
    import todos from './todos';
    import Store from '@ice/store';
    
    const storeManager = new Store();
    const stores = storeManager.registerStores({
      todos
    });
    
    export default stores;
  • In the view component, you can get the store config (including state and actions) by using the useStore hook after importing the store instance. After that, you can trigger actions through event callbacks or by using the useEffect hook, which binds the state to the view template.

    // src/index.js
    import React, { useEffect } from 'react';
    import ReactDOM from 'react-dom';
    import stores from './stores';
    
    function Todo() {
      const todos = stores.useStore('todos');
      const { dataSource, refresh, add, remove, toggle } = todos;
    
      useEffect(() => {
        refresh();
      }, []);
    
      function onAdd(name) {
        add({ name });
      }
    
      function onRemove(index) {
        remove(index);
      }
    
      function onCheck(index) {
        toggle(index);
      }
    
      const noTaskView = <span>no task</span>;
      const loadingView = <span>loading...</span>;
      const taskView = dataSource.length ? (
        <ul>
          {dataSource.map(({ name, done }, index) => (
            <li key={index}>
              <label>
                <input
                  type="checkbox"
                  checked={done}
                  onClick={() => onCheck(index)}
                />
                {done ? <s>{name}</s> : <span>{name}</span>}
              </label>
              <button onClick={() => onRemove(index)}>-</button>
            </li>
          ))}
        </ul>
      ) : (
        noTaskView
      );
    
      return (
        <div>
          <h2>Todos</h2>
          {!refresh.loading ? taskView : loadingView}
          <div>
            <input
              onKeyDown={event => {
                if (event.keyCode === 13) {
                  onAdd(event.target.value);
                  event.target.value = '';
                }
              }}
              placeholder="Press Enter"
            />
          </div>
        </div>
      );
    }
    
    const rootElement = document.getElementById('root');
    ReactDOM.render(<Todo />, rootElement);

Complete example is presented in this CodeSandbox, feel free to play with it.

icestore provides complete type definitions to support IntelliSense in VSCode. TS example is presented in this CodeSandbox.

API

registerStores

Register multiple store configs to the global icestore instance.

  • Parameters
    • namespace {string} unique name of the store
    • stores {object} multiple store's config object including store's state and actions
  • Return value
    • {object} store manager object which including follow methods
      • useStore {function} Hook to use a single store.

        • Parameters
          • namespace {string} store namespace
        • Return value
          • {object} single store instance
      • useStores {function} Hook to use multiple stores.

        • Parameters
          • namespaces {array} array of store namespaces
        • Return value
          • {object} object of stores' instances divided by namespace
      • withStore {function}

        • Parameters
          • namespace {string} store namespace
          • mapStoreToProps {function} optional, mapping store to props
        • Return value
          • HOC
      • withStores {function}

        • Parameters
          • namespaces {array} array of store namespaces
          • mapStoresToProps {function} optional, mapping store to props
        • Return value
          • HOC
      • getState {function} Get the latest state of individual store by namespace.

        • Parameters
          • namespace {string} store namespace
        • Return value
          • {object} the latest state of the store

applyMiddleware

Apply middleware to all the store if the second parameter is not specified, otherwise apply middleware the store by namespace.

  • Parameters
    • middlewares {array} middleware array to be applied
    • namespace {string} store namespace
  • Return value
    • void

Advanced use

async actions' executing status

icestore has built-in support to access the executing status of async actions. This enables users to have access to the loading and error executing status of async actions without defining extra state, making the code more consise and clean.

API

  • action.loading - flag checking if the action is executing
    • Type: {boolean}
    • Default: false
  • action.error - error object if error was throw after action executed
    • Type: {object}
    • Default: null
  • action.disableLoading - flag to disable the loading effect of the action. If this is set to true, relevant view components would not rerender when their loading status changes
    • Type: {boolean}
    • Default: false
  • store.disableLoading - flag to disable the loading effect at global level. An action's disableLoading flag will always take priority when both values are set.
    • Type: {boolean}
    • Default: false

Example

const todos = store.useStore('todos');
const { refresh, dataSource } = todos;

useEffect(() => {
  refresh();
}, []);

const loadingView = (
  <div>
    loading.......
  </div>
);

const taskView = !refresh.error ? (
  <ul>
   {dataSource.map(({ name }) => (
     <li>{name}</li>
   ))}
  </ul>
) : (
  <div>
    {refresh.error.message}
  </div>
);

return (
  <div>
    {!refresh.loading ? taskView : loadingView}
  <Loading />
);

Class Component Support

import React from 'react';
import ReactDOM from 'react-dom';
import Icestore from '@ice/store';

interface Todo {
  id: number;
  name: string;
}

interface TodoStore {
  dataSource: Todo[];
  refresh: () => void;
  remove: (id: number) => void;
}

const todos: TodoStore = {
  // Action && State 
};

const icestore = new Icestore();
const stores = icestore.registerStores({
  todos,
});

class TodoList extends Component<{store: TodoStore}> {
  onRmove = (id) => {
    const {store} = this.props;
    store.remove(id);
  }

  componentDidMount() {
    this.props.store.refresh();
  }

  render() {
    const {store} = this.props;
    return (
      <div>
        {
          store.dataSource.map(({id, name}) => {
            return (<p>
              {name}
              <button onClick={() => onRmove(id)} >Delete</button>
            </p>);
          })
        }
      </div>
      
    );
  }
}

const TodoListWithStore = stores.withStore('todos')(TodoList);
ReactDOM.render(<TodoListWithStore />, document.body);

Middleware

Context

If you have used server side frameworks such as Express or koa, you were probably familiar with the concept of middleware already. Among these frameworks, middleware is used to insert custom code between receiving request and generating response, the functionality of middlewares include data mutation、authority check before the request was handled and add HTTP header、log printing after the request was handled.

In state management area, Redux also implements middleware mechanism, it was used to put custom code between action dispatching and reaching reducer. Its functionalities include log printing, async mechanism such as thunk, promise.

Like Redux, the purpose of icestore implementing middleware mechanism is to add an extensive mechanism between action was not dispatched and dispatched. The difference is that icestore already supports async action, so there is no need to write middleware for async support.

middleware API

icestore takes insiprations from koa for its middleware API design as follows:

async (ctx, next) =>  {
  // logic before action was dispatched

  const result = await next();

  // logic after action was dispatched

  return result;
}

Note: If there is return value in action, all the middlewares in the chain must return the executing result of the next middleware to ensure the action's return value is correctly return from middleware chain.

ctx API
  • ctx.action - the object of the action dispatched
    • Type:{object}
    • Return Value:void
  • ctx.action.name - the name of action dispatched
    • Type:{string}
    • Return Value:void
  • ctx.action.arguments - the arguments of current action function
    • Type:{array}
    • Return Value:void
  • ctx.store - the store object
    • Type:{object}
    • Return Value:void
  • ctx.store.namespace - the store namespace
    • Type:{string}
    • Return Value:void
  • ctx.store.getState - the method to get the latest state value of current store
    • Type:{function}
    • Return Value:void

The example is as follows:

const {
  action, // the object of the action dispatched
  store, // the store object
} = ctx;

const {
  name, // the name of action dispatched
  arguments, // the arguments of current action function
} = action;

const { 
  namespace,  // the store namespace
  getState, // the method to get the latest state value of current store
} = store;

Registration

Due the multiple store design of icestore, it supports registering middlewares for indivisual store as follows:

  1. Global registration: middlewares apply to all stores.

    import Icestore from '@ice/store';
    const stores = new Icestore();
    stores.applyMiddleware([a, b]);
  2. Registration by namespace: The ultimate middleware queue of single store will merge global middlewares with its own middlewares.

    stores.applyMiddleware([a, b]); 
    stores.applyMiddleware([c, d], 'foo'); // store foo middleware is [a, b, c, d]
    stores.applyMiddleware([d, c], 'bar'); // store bar middleware is [a, b, d, c]

Debug

icestore provide an official logger middleware to facilitate user traceing state changes and improve debug productivity.

Installation

npm install @ice/store-logger --save

Guide

Use applyMiddleware API to push logger middleware into middleware queue.

import todos from './todos';
import Icestore from '@ice/store';
import logger from '@ice/store-logger';

const icestore = new Icestore();

const middlewares = [];

// Turn off logger middleware in production enviroment
if (process.env.NODE_ENV !== 'production') {
  middlewares.push(logger);
}

icestore.applyMiddleware(middlewares);
icestore.registerStore('todos', todos);

When action was dispatched, the log will be printed into browser's DevTools by realtime:

The logger includes the following infos:

  • Store Name: namespace of current store
  • Action Name: action being dispatched
  • Added / Deleted / Updated: type of state changes
  • Old state: state before change
  • New state: state after change

Testing

Because all the states and actions are contained in a plain JavaScript object, it is easy to write tests without using mock objects.

Example:

describe('todos', () => {
  test('refresh data success', async () => {
    await todos.refresh();
    expect(todos.dataSource).toEqual([
      {
        name: 'react'
      },
      {
        name: 'vue',
        done: true
      },
      {
        name: 'angular'
      }
    ]);
  });
});

Please refer to the todos.spec.js file in the sandbox above for complete reference.

Best Practices

Never mutate state outside actions

icestore enforces all the mutations to the state to occur only in action methods. Mutation occurred outside actions will not take effect (e.g. in the view component).

The reason is that the mutation logic would be hard to trace and impossible to test as there might be unpredictable changes made to the view components as a result of mutations outside actions.

  // store.js
  export default {
    inited: false,
    setInited() {
      this.inited = true;
    }
  }
  
  // view.js
  const todos = useStore('todos');
  
  useEffect(() => {
    // bad
    todos.inited = true;
    
    // good
    todos.setInited();
  });

Divide store as small as possible

By design, icestore will trigger the rerender of all the view components subscribed to the store (by using useStore) once the state of the store has changed.

This means that putting more state in one store may cause more view components to rerender, affecting the overall performance of the application. As such, it is advised to categorize your state and put them in individual stores to improve performance.

Don't overuse icestore

From the engineering perspective, the global store should only be used to store states that are shared across multiple pages or components.

Putting local state in the global store will break the component's encapsulation, affecting its reusability. Using the todo app as an example, if the app only has one page, the state of the todo app is preferred to be stored as a local state in the view component rather than in the global store.

Browser Compatibility

Chrome Firefox Edge IE Safari Opera UC
9+ ✔

Reference

Contributors

Feel free to report any questions as an issue, we'd love to have your helping hand on icestore.

If you're interested in icestore, see CONTRIBUTING.md for more information to learn how to get started.

License

MIT

icestore's People

Contributors

alvinhui avatar beizhedenglong avatar imsobear avatar lucifer1004 avatar phobal avatar temper357 avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.