redux-simple-state

redux-simple-state automatically generates the redux actions and reducers for you based on the intial state. It lets you add or remove a field in a few seconds and its get/set API makes the redux development super easy. Instead of spending hours on maintaining actions and reducers, now you can focus on more important works.

Usage no npm install needed!

<script type="module">
  import reduxSimpleState from 'https://cdn.skypack.dev/redux-simple-state';
</script>

README

redux-simple-state

redux-simple-state automatically generates the redux actions and reducers for you based on the intial state. It lets you add or remove a field in a few seconds and its get/set API makes the redux development super easy. Instead of spending hours on maintaining actions and reducers, now you can focus on more important works.

Quick start

Create the store and inject the todos state

import { ReduxManager, createState } from "redux-simple-state";

// Create store
const store = ReduxManager.createStore();

const INITIAL_STATE = {
  todos: [],
  visibilityFilter: "SHOW_ALL"
};

// Generate simple state based on the initial state
const state = createState("todosState", INITIAL_STATE);

//Injects the todos state to the state tree
ReduxManager.registerState(state);

/*
 *  The state tree now looks like:
 * {
 *   todosState:{
 *       todos:[],
 *       visibilityFilter: "SHOW_ALL"
 *   }
 * }
 */

To read the value of visibilityFilter:

let filter = state.visibilityFilter.get();

To change the value of visibilityFilter:

state.visibilityFilter.set("SHOW_COMPLETED");

To access the selector of visibilityFilter

let visibilityFilterSelector = state.visibilityFilter.selector;

To insert a new todo to todos

state.todos.addItem({ id: 0, text: "first todo", completed: false });

To add a new field into the state tree, you can just modify the INITIAL_STATE

const INITIAL_STATE = {
  todos: [],
  visibilityFilter: "SHOW_ALL",
  user: null
};

/*
 *  The state tree now looks like:
 * {
 *   todosState:{
 *       todos:[],
 *       visibilityFilter: "showSHOW_ALL",
 *       user: null
 *   }
 * }
 */

// Get value
let userDetail = this.state.user.get();
// Set value
this.state.user.set({ id: "1" });

You can find the completed example in ./examples folder.

  1. todomvc
  2. todomvc-typescript

Introduction

In the most situations, we use Redux as a global state store where we can save our data globally and share it among the app. However the cost is that we have to deal with actions and reducers. Especially for a project with a complex state structure, maintaining the actions, reducers and constants can be very cumbersome.

We just need a place to save data, can we have a simple way to do it?

redux-simple-state is a utility to simplify the process when working with Redux-based projects. The goal of this library is to make the Redux as transparent as possible, so that you can read/write states without knowing actions and reducers. It does NOT change the behavior of Redux. Below is a list of highlighted features.

  • Dynamically generates actions and reducers based on the initial state, which allows you to add a new filed to the state or change existing state in a few seconds.
  • Every filed in the state tree has a default selector that you can bind to a view or use in a side-effect library such as redux-saga.
  • Has a higher level get function and a set function to read and write the value of a field without exposing details of state store and dispatching mechanism.
  • ReduxManager allows you to getState or dispatch an action from anywhere without accessing the Store object directly
  • ReduxManager also allows you to inject new state or reducer on the fly

Note: seamless-immutable is NOT supported yet.

Install

Install redux and reselect first.

yarn add redux reselect
yarn add redux-simple-state

Or

npm install --save redux reselect
npm install --save redux-simple-state

Use with connected-react-router

import { applyMiddleware, compose } from "redux";
import { ReduxManager } from "redux-simple-state";
import { connectRouter } from "connected-react-router";
import { routerMiddleware } from "connected-react-router";

export default function configureStore(initialState = {}, history) {
  const enhancers = [applyMiddleware(routerMiddleware(history))];

  // If Redux DevTools Extension is installed use it, otherwise use Redux compose
  const composeEnhancers =
    process.env.NODE_ENV !== "production" &&
    typeof window === "object" &&
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
      : compose;

  const store = ReduxManager.createStore(
    initialState,
    composeEnhancers(...enhancers)
  );
  ReduxManager.registerReducer("router", connectRouter(history));
  // ... rest of your simple states or reducers
  // ReduxManager.registerState(myState);

  return { store };
}

Use with redux-persist

import { applyMiddleware, compose } from "redux";
import {ReduxManager, createState} from 'redux-simple-state'
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import { PersistGate } from "redux-persist/integration/react";

... //create store

let persistor = persistStore(store);

const INITIAL_STATE={
    todos:[],
    visibilityFilter: "SHOW_ALL"
}

const todosState = createState("todos", INITIAL_STATE)

const persistConfig = {
  key: "todos",
  whitelist: ["visibilityFilter", "todos"],
  storage
};

// Note: The properties with the prefix underscore are StateContainer's own properties.
// The underscroe is used to differentiate from those dynamically added properties.
ReduxManager.registerReducer(
  todosState._name,
  persistReducer(persistConfig, todosState._reducer)
);

... //wrap your root component with PersistGate

Handle side effects

With redux-simple-state, you don't have to use any side effects libraries to handle async and complex synchronous logic. Below is an example to show you how to handle side effect with redux-simple-state. Please note that redux-simple-state is not a middleware of redux, so no extra config is required.

Highlights

  1. You can use async functions to handle those asynchronous flows. No generator functions, no yields.
  2. No callback hell.
  3. Easy to test
// myState.js
const INITIAL_STATE={
    items:[],
    loading: false,
    requestError: null
}

const myState = createState("myState", INITIAL_STATE);

export default myState
// store.js
import { ReduxManager, createState } from "redux-simple-state";
import myState from 'myState.js';

const store = ReduxManager.createStore();
ReduxManager.registerState(myState);
//controller.js

// Promise version
export function fetchData(someValue) {
    myState.loading.set(true);
    return myAjaxLib.post("/someEndpoint", {data : someValue})
            .then(response => {
              mystate.items.set(response.data);
              myState.loading.set(false);
              myState.requestError.set(null);
            })
            .catch(error => {
              myState.loading.set(false);
              myState.requestError.set(error);
            });
}

// Async version
export async function fetchDataAsync(someValue) {
    myState.loading.set(true);
    try{
      let response = await myAjaxLib.post("/someEndpoint", {data : someValue});
      // let anotherResponse = await myAjaxLib.post("/anotherEndpoint");
      mystate.items.set(response.data);
      myState.loading.set(false);
      myState.requestError.set(null);
    }
    catch(error)
    {
      myState.loading.set(false);
      myState.requestError.set(error);
    }
}

export function addTodosIfAllowed(todoText) {
      let todos = myState.todos.get();
        if(todos.length < MAX_TODOS) {
            myState.todos.addItem({text : todoText})
        }
    }
}
// myComponent.js
import React from 'react'
import * as myController from 'controller.js'

...

export const myComponent = ({controller})=>{
  return <button onClick={controller.fetchData}>Click to fetch data</button>
}

myComponent.defaultProps= {
  controller: myController
}

If you want to get

API

We generate actions and reducers for a field based on the type of its initial value. These five types are supported.

  1. Object
  2. String
  3. Number
  4. Array
  5. Boolean

If the default value is null, the field is marked as an Object.

createState

Creates a state. The state is an instance of StateContainer. You can inject the state to the store when necessary, and use it to get or set value of a field in it.

// THe initial value can be a nested object
let state = createState("demo", {
  filter: "all_completed",
  profile: {
    id: 1,
    name: "",
    is_active: true
  }
});

ReduxManager.registerState(state);

/*
 *  The state tree now looks like:
 * {
 *   demo:{
 *       filter: "all_completed",
 *       profile:{
 *            id: 1,
 *            name: "",
 *            is_active: true
 *        }
 *   }
 * }
 */

Params:

  • name (String): The name of the field
  • initialValue (not Function): The default value of the field.

Returns:

A StateContainer instance.

ReduxManager

The singleton instance which allows you to access store from anywhere.

ReduxManager.createStore([preloadedState], [enhancer])

Creates a Redux store that holds the complete state tree of your app. This function is similar as the createStore from Redux except that it doesn't accept default reducer. Please user ReduxManager.register or ReduxManger.registerState to inject the reducers.

Params:

  • preloadedState (string): The initial state.
  • enhancer (Function): The store enhancer.

Returns:

  • Store (Object): Same as the Redux store object.

ReduxManager.registerReducer(name, reducer)

Injects the reducer to the store using the given name. This function is useful when you want to partialy migrate your project to the redux-simple-state, or you have third-party reducer to add in, such as connected-react-route.

Params:

  • name (String): The field name.
  • reducer (Function): A reducing function that returns the next state tree.

Returns:

  • None

Example:

import { ReduxManager } from "redux-simple-state";

const initialState = { todos: [], visibilityFilter: "SHOW_ALL" };

function todoAppReducer(state = initialState, action) {
  switch (action.type) {
    case "SET_VISIBILITY_FILTER":
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      });
    case "ADD_TODO":
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      });
    default:
      return state;
  }
}

ReduxManager.registerReducer("todos", todoAppReducer);

ReduxManager.registerState(state)

Injects new state to the store.

Params:

  • state (StateContainer): The state returned by createState function. The name of state will be used as the field name.

Returns:

  • None

Example:

import { ReduxManager, createState } from "redux-simple-state";

const initialState = { todos: [], visibilityFilter: "SHOW_ALL" };

const todosState = createState("todos", initialState);

ReduxManager.registerState(todosState);

// To add a todo
todosState.todos.addItem({
  text: "Buy milk",
  completed: false,
  id: 1
});

// To change visibilityFilter
todosState.visibilityFilter.set("SHOW_COMPLETED");

ReduxManager.dispatch(action)

Dispatches an action to the store. Same as store.dispatch in Redux. Please check Redux document for more details.

Params:

  • action (Object): A plain object describing the change that makes sense for your application.

Returns:

  • (Object): The dispatched action (see notes).

Example:

import { ReduxManager } from "redux-simple-state";

let myAction = { type: "SET_VISIBILITY_FILTER", filter: "SHOW_COMPLETED" };

ReduxManager.dispatch(myAction);

ReduxManager.getState()

Returns the current state tree of your application. Same as store.getState in Redux. Please check Redux document for more details.

Returns:

  • (Any): The current state tree of your application.

Example:

import { ReduxManager } from "redux-simple-state";

const currentState = ReduxManager.getState();

ReduxManager.select(selector)

Returns the selected value specified by the selector function.

Params:

  • selector (Func): A function that accepts state and returns the selected field value. For example, (state)=>state.user.id;

Returns:

  • (Any): value.

Example:

import { ReduxManager } from "redux-simple-state";

let userFullNameSelector = state =>
  `${state.user.firstName} ${state.user.lastName}`;

const userFullName = ReduxManager.select(userFullNameSelector);

ReduxManager.resetState()

Reset the state tree to its initial value.

Returns:

  • (Action): The reset action.

Example:

import { ReduxManager, createState } from "redux-simple-state";

const initialState = { todos: [], visibilityFilter: "SHOW_ALL" };

const todosState = createState("todos", initialState);

ReduxManager.registerState(todosState);

// To add a todo
todosState.todos.addItem({
  text: "Buy milk",
  completed: false,
  id: 1
});

// To change visibilityFilter
todosState.visibilityFilter.set("SHOW_COMPLETED");

ReduxManager.resetState();
/* The state now is reset to default
{
  todos:{
    todos: [],
    visibilityFilter: "SHOW_ALL"
  }
}
*/

StateContainer

A container of a state which gives you all the conveniences to operate the state. You should only create a state container via createState function. The function will wire the actions and reducers based on the initial value.

Props:

selector

A selector function which accepts a state object and returns the value of the field

let myState = createState("demo", {
  filter: "all_completed",
  profile: {
    id: 1,
    name: "",
    is_active: true
  }
});

// For the root field
let selector = myState.selector;

// For sub field
let filterSelector = myState.filter.selector;
let profileIdSelector = myState.profile.id.selector;

Use with react-redux

function mapStateToProps(state) {
  return { profile: myState.profile.selector(state) };
}

Or together with reselect

import { createStructuredSelector } from "reselect";

const mapStateToProps = createStructuredSelector({
  profile: myState.profile.selector
});

Use with redux-saga

function* handler() {
  yield select(myState.filter.selector);
}

Write your own selectors

import { createSelector } from "reselect";
import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from "./constants/TodoFilters";
import state from "./todosState";

export const getVisibleTodos = createSelector(
  [state.visibilityFilter.selector, state.todos.selector],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case SHOW_ALL:
        return todos;
      case SHOW_COMPLETED:
        return todos.filter(t => t.completed);
      case SHOW_ACTIVE:
        return todos.filter(t => !t.completed);
      default:
        throw new Error("Unknown filter: " + visibilityFilter);
    }
  }
);

export const getTodoCount = createSelector(
  [state.todos.selector],
  todos => todos.length
);

Functions:

get()

Returns the value of the field. Please make sure you call this function only after the state is registered.

let myState = createState("demo", {
  filter: "all_completed",
  profile: {
    id: 1,
    name: "",
    is_active: true
  },
  books: []
});

//For the root field
let value = myState.get();

//For sub field
let filter = myState.filter.get(); // returns "all_completed"
let profileId = myState.profile.id.get(); // returns 1
let books = mySate.books.get(); // return []

set(value)

Set the value of the field. We don't check the type of the new value before writing to the filed. Please make sure to use correct value. For example, the value should be an object if the field is an object.

let myState = createState("demo", {
  filter: "all_completed",
  profile: {
    id: 1,
    name: "",
    is_active: true
  },
  books: []
});

myState.filter.set("show_all");
myState.profile.id.set(2);
myState.books.set(["Learn JavaScript"]);

// for nested object
myState.profile.set({
  id: 2,
  name: "test",
  is_active: false
});

// If you just want to update the object, you can use `myState.profile.update` instead of set.
// There are more details below.

resetToDefault()

Resets the value of the filed to its initial value.

Example:

import { ReduxManager, createState } from "redux-simple-state";

const initialState = { todos: [], visibilityFilter: "SHOW_ALL" };
const todosState = createState("todos", initialState);
ReduxManager.registerState(todosState);

todosState.visibilityFilter.set("SHOW_COMPLETED");
todosState.visibilityFilter.get(); // return SHOW_COMPLETED

todosState.visibilityFilter.resetToDefault();
todosState.visibilityFilter.get(); // return SHOW_ALL

Object Field

Except the shared functions, the Object field has one more function.

update(value)

Update the object. The new value will be merged. Same as doing this:

let newState = { ...state, ...value };

Params:

  • value(Object)
state.profile.update({
  is_active: false
});

Array Field

Except the shared functions, the Array field has a few more functions to manipulate its items.

addItem(item)

Adds the news item to the end of the array. Params:

  • item(Any)

Example:

... // create todosState

todosState.todos.addItem({
  text: "Buy milk",
  completed: false,
  id: 1
})

updateItems(query, value):

Updates all matched items by the given value. If the value is an object, it will be merged to existing object.

Params:

  • query (Function): Query is a function accepts an item in the array, and returns a boolean which indicates if the item is selected.
  • value (Any)

Example:

... // create todosState

// Mark todo 1 as completed
todosState.todos.updateItems((todo)=>todo.id === 1, { completed:true });

// Mark incompleted todos as completed
todosState.todos.updateItems((todo)=>!todo.completed, { completed:true });

updateAll(value)

Updates all the items in the array by the given value. If the value is an object, it will be merged to existing object.

Params:

  • value (Any)

Example:

... // create todosState

// Mark all todos as completed
todosState.todos.updateAll({ completed:true });

deleteItems(query)

Deletes all matched items.

Params:

  • query (Function): Query is a function accepts an item in the array, and returns a boolean which indicates if the item is selected.

Example:

... // create todosState

// Delete all completed todos
todosState.todos.deleteItems((todo)=>todo.completed);

deleteAll()

Deletes all items.

Example:

... // create todosState

// Delete all todos
todosState.todos.deleteAll();