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.
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.
- todomvc
- todomvc-typescript
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 aset
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 directlyReduxManager
also allows you to inject new state or reducer on the fly
Note: seamless-immutable
is NOT supported yet.
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
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
- You can use
async
functions to handle those asynchronous flows. No generator functions, no yields. - No callback hell.
- 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
We generate actions and reducers for a field based on the type of its initial value. These five types are supported.
- Object
- String
- Number
- Array
- Boolean
If the default value is null
, the field is marked as an Object.
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.
The singleton instance which allows you to access store from anywhere.
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.
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);
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");
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);
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();
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);
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"
}
}
*/
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.
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
);
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
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
});
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();