Modular and easy-to-grasp redux based state management, with least boilerplate
Note: If migrating from version 1.x.x to 2, you would find breaking changes.
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.
It organizes your store as collection of independent modules which can be used across different stores/applications/platforms.
Illustration by Vikas
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.
@connectStore
decorator or using render 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)
dispatchers
(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, createModule } from "redux-box";
import { call } from "redux-saga/effects";
// define initial state
const state = {
name: "John",
email: "[email protected]",
todos: [{ name: "First", type: 1 }, { name: "Second", type: 0 }]
};
// define dispatchers
export const dispatchers = {
setName: name => ({ type: "SET_NAME", name }),
setEmail: email => ({ type: "SET_EMAIL", email })
};
// define mutations
const mutations = {
SET_NAME: (state, action) => (state.name = action.name),
SET_EMAIL: (state, action) => (state.email = action.email)
};
// define sagas
const sagas = createSagas({
SET_EMAIL: function*(action) {
const response = yield call(api.updateEmail, action.email);
}
});
// selectors
export const getTodos = (state) => state.todos
export const getCompletedTodos = createSelector( getTodos, (todos) => {
return todos.filter(todo => todo.type==1)
})
const selectors = {
getTodos,
getCompletedTodos
};
export default createModule({
state,
mutations,
sagas,
});
import { createStore } from "redux-box";
import userModule from "./user";
export default createStore({
user: userModule
});
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)
createStore((modules: Object), (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: yourCustomReducer
},
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,
devToolOptions: {}
};
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 { connectStore } from "redux-box";
import { getTodos, getCompletedTodos, dispatchers } from "./user";
@connectStore({
mapState: state => ({
user: state.user
}),
mapSelectors: {
todos: getTodos,
completedTodos: getCompletedTodos
},
mapDispatchers: {
setName: dispatchers.setName,
setEmail: dispatchers.setEmail
}
})
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={()=>{
this.props.setName('jane doe')
}}> Change Name </button>
</div>
);
}
}
Docs for V2 are in progress, would be updated in a few days
Here are some examples to let you play around with redux-box
No pending feature requests
Either wrap the root component in a <Provider>, or explicitly pass “store” as a prop to "Connect(MyComponent)
This error occurs because of mismatch among versions of dependencies of redux-box, most likely react-redux
. You can run this command to fix this issue for now:
yarn add [email protected]
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)
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;