0.3.0 • Published 6 years ago

storext v0.3.0

Weekly downloads
3
License
MIT
Repository
github
Last release
6 years ago

Storext

An immutable state management library for your React app that utilize the unidirectional data flow which fits on React ecosystem. It produces a predictable state container which helps managing the state of your React app. It is powered by Immutable.js.

Storext gives you ease in handling your states, and on top of that, it is also easy to test.

NPM JavaScript Style Guide Build Status

Influence

Storext used the ideas on Flux Architecture, Redux, and Vuex. It uses new React context api for handling context and Immutable.js for efficient re-rendering.

Note

Storext will only work in React >=16.3

Installation

npm install --save storext

Guide 📖

Table of Contents

Getting Started

Start by looking through the guides and examples on Github.

How storext works

Each state of the features of your app is managed by a specific store. The state is transformed into immutable state. The only way to update the state of a store is to commit an updater function which returns a state change object. Updater can be commit directly by React components or commit by Thunks. Once the state of a store changes, all consumed components observing on a specificic data gets re-render. Storext treats collections as values. It achieve this behavior using Immutable.js. To check the architectural pattern uses by storext, click on this link.

Storext Parts

  • Store - A store is what holds the data of an application. The only way to mutate the data on the store is to respond on the stateChange object.
  • Info - An object which holds the payload of information. It contains the type of updater or thunk and data which is sent to store. This is the only data use in store.
  • Updater - It specifies how to update the current state of a store. It receives a data coming from the info object. It returns a stateChange object. Updaters are invoke using commit function.
  • Thunk - A function which can contain arbitrary asynchronous operations and conditional logic. Inside thunk, it can commit an updaters. It receives a data coming from the info object. The idea behind thunk is influenced by redux-thunk. Thunks are invoked using dispatch function.
  • React View - The state is displayed on the React view.

Quick Start

import { createStore, consume } from 'storext'

// create initial Counter state. This object is transformed into immutable.js collections. Uses fromJS in transforming.
const initialState = {
  count: 0
}

// create Counter store. This object has a fields of state, updaters, and thunks. Thunks is optional.
const counterStore = {
  state: initialState,
  /**
 * This is an updater function. It specifies how to update the current state of a store.
 * Function signature is (payload, localState) => stateChange. This is the only way to change the state of 
 * a store. The state is immutable collection. Update it using immutable.js API. In this way, we can 
 * avoid mutating the state directly.
 */
  updaters: {
    increment (payload, state) {
      // updates the count data of counter store.
      return state.update('count', (value) => value + payload.count)
    },
    decrement (payload, state) {
      return state.update('count', (value) => value - payload.count)
    }
  }
}

// declare the stores use by the React app.
const stores = {
  counter: counterStore
}

/**
* Creating main store based on the given stores. Main store creates
* immutable state tree based on the state of every stores. It returns a main store object that has Provider component. 
* Provider component exposes the immutable state 
*/
const { Provider } = createStore(stores)

/**
 * Storext used the Container-Presentational design pattern which conceptualize by React. 
 * It complements the architecture of storext. For more info 
 * - https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0.
 */
 
/**
 * Consuming the state and handlers to our Counter component. We must define a 
 * mapStateToProps and mapHandlersToProps. It returns a Container component.
 * It is concerned with how things work. Provides the data and behavior to
 * presentational or other container components.
 */

// Mapping the state to props. Function signature is (globalState, ownProps) => mapState
const mapStateToProps = (state) => ({
  count: state.counter.get('count') // access the counter store state. Get the count value on Map counter.
})

/**
 * Mapping handlers to props. Function signature is (commit, dispatch, globalState) => handlers. 
 */ 
const mapHandlersToProps = (commit) => ({
  handleDecrement () {
    // when commiting an updater, we need to pass an info object specifying the updater type and payload.
    commit({
      type: 'decrement',
      payload: {count: 10}
    })
  },
  handleIncrement () {
    commit({
      type: 'increment',
      payload: {count: 10}
    })
  }
})

const ContainerCounter = consume(mapStateToProps, mapHandlersToProps)(Counter)

/**
 * Presentational Component is concerned with how things look.
 */
const Counter = ({count, handlers: {handleDecrement, handleIncrement}}) => (
  <div>
    <button onClick={handleDecrement}>Decrement</button>
    <span>{count}</span>
    <button onClick={handleIncrement}>Increment</button>
  </div>
)

/**
 * Mount the components.
 */
const App = () => (
  <Provider>
    <ContainerCounter />
  </Provider>
)

Handling Asynchronous and Composing Updater

When developing an app, we often encounter evaluating some asyc computations. Then we need to update the state of store based on the response of async. Storext provides handy solution on how to handle async computations. Here comes Thunks. A thunk can contain arbitrary asynchronous operations and it can commit updater functions.

/**
* It is common pattern to declare constant updater types. This allow us to group and 
* see entire updaters and thunks which are used in the app.
*/
const PENDING = 'PENDING'
const SUCCESS_GET_USERS = 'SUCCESS_GET_USERS'
const FETCH_USERS = 'FETCH_USERS'

const pending = ({pending}, state) => state.setIn(['process', 'pending'], pending)
const success = ({success}) => (state) => state.setIn(['process', 'success'], success
const getUsers = ({users}) => (state) => state.set('users', users)

/**
 * Because our updaters are pure function, we can compose an updater based on result
 * of other updaters. We call it Updater Composition. We can use compose helper function 
 * from 3rd-party library like ramda or lodash but its optional. Learn composition pattern
 * - https://medium.com/davao-js/make-fp-fun-what-the-heck-is-composition-f8707674a177
 *
 * Try to avoid calling multiple updaters in a row. This can cause issue in 
 * logger states utility. Check the logger utility section for more info.
 */
const successGetUsers = ({pending, success, users}, state) => {
  return compose(
    getUsers({users}),
    success({success})
  )(state)
}

const userStore = {
  state: {
    users: {},
    process: {
      pending: false,
      success: false
    }
  },
  updater: {
    // We can use the ES2015 computed property name in declaring methods
    [PENDING]: pending,
    [SUCCESS_GET_USERS]: successGetUsers
  },
  thunks: {
    // The thunk returns an inner function which has signature (commit, globalState, thunkExtraArg) => any. 
    [FETCH_USERS] () {
      return function inner (commit) {
        // commit PENDING updater. Pass an info object.
        commit({type: PENDING, payload: {pending: true}})
        // Whatever value return by innerFn, it will be the return value of dispatch function. 
        return ajaxFetchUsers()
          .then((users) => {
            // commit SUCCESS_GET_USERS updater. Pass an info object.
            commit({
              type: SUCCESS_GET_USERS,
              payload: {
                success: true,
                users
              }
            })
          })
      }
    }
  }
}

// Declare handlers
const mapHandlersToProps = (commit, dispatch) => ({
  handleFetch () {
    // when dispatching a thunk, we need to pass an info object like in committing updater which specifies the thunk type and payload. 
    // Payload is optional.
    dispatch({type: FETCH_USERS})
      // once the inner function of thunk returns a Promise, we can chain a 
      // Promise here as long it returns a value.
      .then(() => console.log('DONE!'))
  }
})

Using selectors

Rule of thumb in managing state through Storext is to store minimal state as possible to every stores. Through selectors, it can compute the derived state from the store which is possible to avoid state complexity.

// create users count selector
const usersCountSelector = (state) => state.get('users').count()

// pass the state of users store.
const mapStateToProps = ({users}) => ({
  count: usersCountSelector(users)
})

const Container = consume(mapStateToProps, {})(Presentational)

To create more efficient selectors, take a look of Reselect library.

Utilities

Storext includes some utilities which can help your productivity and reduce some boilerplates. Some utilities included are infoCreator and infoCreators. These utilities are used for creating info object which is a payload of information.

Sometimes creating info object which pass to updater/thunk is little bit cumbersome. Helpers infoCreator and infoCreators help to reduce the complexity and make it reusable.

Using infoCreator:

import { infoCreator } from 'storext'

/**
 *
 * infoCreator helper is an higher order function which returns infoCreator function. infoCreator creates an infoObject.
 * Function signature - (infoType) => infoCreator . It also modifies the output of 'toString' method of the return function. 
 * It will output the given infoType and transform to camelCase. For are instance, given infoType is 
 * ADD_TODO -> the toString value of created infoCreator is 'addTodo'. Therefore, we can use addTodo as expression in
 * computed property name.
 */
 
const addTodo = infoCreator('ADD_TODO')

const updaters = {
  // Instead hardcoding the name of updater function, we can use the reference addTodo as computed property name.
  [addTodo]: (payload, state) => ...code
}

const mapHandlersToProps = (commit) => ({
  handleAddTodo () {
    // create info object with payload
    const info = addTodo({id: 1, text: 'Travel'}) // {type: 'ADD_TODO', payload: {id: 1, text: 'Travel'}}
    // Pass the info object to commit an updater.
    commit(info)
  }
})

Using infoCreators:

import { infoCreators } from 'storext'

/**
 * infoCreators returns an object which map types to info creators. The keys of the object is camel-case from the elements included on the given array types. Function signature is (types) => Object.
 */

const { addTodo, removeTodo } = infoCreators(['ADD_TODO', 'REMOVE_TODO']) // {addTodo: function receiver () {}, removeTodo: function receiver () {}}

const updaters = {
  [addTodo]: (payload, state) => ...code,
  [removeTodo]: (payload, state) => ...code
}

const mapHandlersToProps = (commit) => ({
  handleAddTodo () {
    const info = addTodo({id: 1, text: 'Travel'}) // {type: 'ADD_TODO', payload: {id: 1, text: 'Travel'}}
    commit(info)
  },
  handleRemoveTodo () {
    const info = removeTodo({id: 1}) // {type: 'REMOVE_TODO', payload: {id: 1}}
    commit(info)
  }
})

Changelog

This project adheres to Semantic Versioning. Every release, along with the migration instructions, is documented on the Github Releases page.

License

MIT