Setting up and organizing a redux store in your react/react-native projects can be a tedious and daunting task. Redux-Box aims at abstracting away the complexity in using redux with redux-saga, while letting you manage application state in modular fashion, without losing the flexibility or without introducing new bizarre terms.
Illustration by Vikas
- Clean, expressive and minimal reducers: If you prefer keeping your code expressive, you will feel right at home with redux-box. Have a look at a simple reducer written with and without redux-box:
If you are concerned about the state getting mutated directly in the snippet above, then you need not be. Because the state
being passed to a mutation is NOT the actual state object
of application, instead it's a Proxy of the state. Redux-box relies on wonderful immer library to achieve the expressiveness you see above.
- Organise your giant state into modules
- Setup redux+redux-saga for our react/react-native app in a trice
- Simplified Sagas
- Just import and use store:
You wouldn't need to write a dedicated HOC to interact with your store. If you need to interact with a particular store-module, you can simply import it and use it. As simple as that! Redux box offers two ways of using a module in your component : using
@connectStore
decorator or usingrender props
. (refer to the usage section for better reference)
npm install --save redux-box
OR
yarn add redux-box
To support the latest decorator and generator syntax, you would want to use the .babelrc
file as below:
{
"presets": [
"babel-preset-react-native-stage-0/decorator-support"
],
"env": {
"development": {
"plugins": [
"transform-react-jsx-source",
"transform-es2015-typeof-symbol"
]
},
"production": {
"plugins": ["transform-remove-console"]
}
}
}
Redux box emphasizes on dividing the whole application into multiple modules. Each of these modules manages it's state seperately, with the help of 5 segments (You can skip the segments you don't need in your project):
-
state (It specifies the initial state of the module)
-
mutations (It specifies the function to be run when a specific action is dispatched, it's same as reducer but clutter-free)
-
actions (it contains the actionCreators for your store. Each method of this object must return an action object )
-
sagas (this is where you write all your sagas / async operations)
-
selectors ( selectors can be thought of as getters or computed properties from your state)
Make sure you specify a unique name for each module ('user' in this example)
// store/user.js
import { createSagas, createContainer } from "redux-box";
import { call } from "redux-saga/effects";
const state = {
name: "John",
email: "[email protected]",
todos: [{ name: "First", type: 1 }, { name: "Second", type: 0 }]
};
const actions = {
setName: name => ({ type: "SET_NAME", name }),
setEmail: email => ({ type: "SET_EMAIL", email })
};
const mutations = {
SET_NAME: (state, action) => (state.name = action.name),
SET_EMAIL: (state, action) => (state.email = action.email)
};
const sagas = createSagas({
SET_EMAIL: function*(action) {
const response = yield call(api.updateEmail, action.email);
}
});
//selectors
const getTodos = (state) => state.todos
const getCompletedTodos = createSelector( getTodos, (todos) => {
return todos.filter(todo => todo.type==1)
})
// include the ones you would like to access in your components here
const selectors = {
getTodos,
getCompletedTodos
};
export const module = {
name: "user",
state,
actions,
mutations,
sagas,
selectors
};
//OPTIONAL: if you want to access this module using render props in your components:
export default createContainer(module);
NOTE: There also exists an optional shorthand to create actions rapidly, like so:
import {createActions, using} from 'redux-box'
const actions= createActions({
setName: using('name'),
setEmail: using('email')
})
The actionCreators generated by this code are same as the ones generated by the above code.
import { createStore } from "redux-box";
import { module as userModule } from "./user";
import { module as postModule } from "./post";
export default createStore([userModule, postModule]);
OPTIONAL: if you need to create store with some reducers and middlewares, the signature of createStore method from redux-box goes like this:(if you have already included a module in modules array, you need NOT to register it's sagas or reducers manually by including in config object)
import { moduleToReducer } from "redux-box";
createStore((modules: Array), (config: Object));
//example config object
config = {
//define redux middlewares
middlewares: [],
//define the default state for your store
preloadedState: {},
// sagas to be manually registered
sagas: [userModule.sagas, testModule.sagas],
// reducers to be manually registered
reducers: {
user: moduleToReducer(user)
},
decorateReducer: reducer => {
//do something
return newReducer;
},
//overrite the compose function
composeRedux: (composer) => {
// do something
// return modified compose function
},
// Dynamically decide when to enable or disable dev-tools
enableDevTools: () => true
};
After this you would need to wrap your root component around the Provider tag like so :
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import RootComponent from "./components/RootComponent";
class App extends React.component {
render() {
return (
<Provider store={store}>
<RootComponent />
</Provider>
);
}
}
export default App;
import React, { Component } from "react";
import { module as userModule } from "store/user";
import { connectStore } from "redux-box";
@connectStore({
user: userModule // AppComponent receives 'user' as a prop
})
export default class AppComponent extends Component {
componentDidMount() {
console.log(this.props.user);
/*
{
name : 'John',
email : '[email protected]',
getTodos: [{ name: "First", type: 1 }, { name: "Second", type: 0 }],
getCompletedTodos: [{ name: "First", type: 1 }],
setName : fn(arg),
setEmail : fn(arg)
}
*/
}
render() {
const { user } = this.props;
return (
<div>
<h1>{user.name}</h1>
<h2>{user.email}</h2>
<button onClick={()=>{
user.setName('jane doe')
}}> Change Name </button>
</div>
);
}
}
import React, {Component} from 'react'
import UserModule from 'store/user'
export default class AppComponent extends Component{
render(){
return(
<div>
<UserModule>
{(user)=> (
<p> {user.name} </p>
<p> {user.email} </p>
)}
</UserModule>
</div>
)
}
}
Here are some examples to let you play around with redux-box
- Basic example - https://stackblitz.com/edit/react-3c8vsn?file=Hello.js
- Example showing redux-saga usage: - https://stackblitz.com/edit/react-qmedt4?file=Hello.js
- Example usage with redux-form: https://stackblitz.com/edit/react-w4dqth?file=store%2Findex.js
- Example usage with redux-persist : https://stackblitz.com/edit/react-pezrbb?file=store%2Findex.js
- Example showing usage of preloaded state for SSR: https://stackblitz.com/edit/react-qcasn4?file=store/index.js
- Using redux-observable: https://stackblitz.com/edit/react-zu8qjn?file=store%2Fuser%2Fepics.js
No pending feature requests
- Decorators aren't working
Decorators aren't still a part of es6. To use the decorator syntax you should be using a transpiler like babel. Also, in create-react-app projects the .babelrc
file doesn't really work so you would need to run npm run eject
to be able to use custom babel-plugins. Following .babelrc
should suffice:
{
"plugins": ["transform-decorators-legacy", "styled-components"],
"presets": [ "react","es2015", "stage-2" ]
}
In case you wouldn't like to eject, you can still use redux-box without decorators. Like so:
@connectStore({
ui: uiModule
})
class TestComponent extends React.Component{
...
}
export default TestComponent
Above snippet is equivalent to:
class TestComponent extends React.Component{
...
}
export default connectStore({
ui: uiModule
})(TestComponent)
- Can I use all the features of redux-box, with
createStore
from redux instead?
Yes, you can! Here's the script showing how you can use createStore
from redux, to setup your modules (with reducers, sagas and middlewares):
(v1.3.9 onwards)
import { applyMiddleware, combineReducers, compose, createStore } from "redux";
import createSagaMiddleware from "redux-saga";
import { all } from "redux-saga/effects";
import { moduleToReducer } from "redux-box";
import { module as homeModule } from "./home";
import { module as userModule } from "./user";
//hook up your module reducers
const combinedReducer = combineReducers({
home: moduleToReducer(homeModule),
user: moduleToReducer(userModule)
});
// hook up your module sagas
const sagas = [...homeModule.sagas, ...userModule.sagas];
// hook up your middlewares here
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
//what follows below is the usual approach of setting up store
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
let enhancer = composeEnhancers(applyMiddleware(...middlewares));
function* rootSaga() {
yield all(sagas);
}
const store = createStore(combinedReducer, enhancer);
sagaMiddleware.run(rootSaga);
export default store;