Organising state in react and react-native applications with Redux Box
Since this article has been published, there have been few additions/improvements in the redux-box API. Please refer to the docs for further details : https://github.com/anish000kumar/redux-box
Redux is amazing!
Chances are you have already heard someone saying this. Redux is indeed amazing and quite simple in itself. But, it can be annoyingly difficult to wrap your head around the whole ecosystem that revolves around redux.
If you want to try out ‘hello world’ of a redux powered app, it won’t take much time or effort. But as soon as you start thinking of a large scale app with redux, you will be bombarded with hundreds of resources all stating different structures and ideologies. And even if you have it setup for once, you will have to redo the same manual work, for each new project, that can be actually avoided.
Besides, some of the trivial tasks like getting the data from state or updating the state can require too much of manual work. For example , typically you would create a reducer first, this reducer will then have a case statement to handle your action. Then you would create an action and an actionCreator to fire the action. Then you would need to create a container component which will map state to props and dispatch to props, and then only you would be able to manipulate the state. Personally, I am not a fan of this process, seems too manual and most of the steps are actually repetitive.
Meet Redux Box !
It aims at making the redux setup process a breeze, for any react/react-native app.
Installation
Run this command in your terminal/cmd to install the package:
npm install --save redux-box
The Basics
Redux box emphasizes on dividing the whole application into multiple modules. Each of these modules manages it’s own state. The module is composed of four parts:
- state.js (this file specifies the initial state of the module)
- mutations.js (this file specifies the function to be run when a specific action is dispatched, it’s same as reducer but clutter-free)
- sagas.js ( this is where you write all your sagas / async operations)
- index.js — Container (this file exports a container which encloses the whole module and can be used with render props)
Usage
- Setting up the files : The directory structure would look something like below.
store
directory has anindex.js
file and various modules as shown below.
-src
|-store
| |-index.js
| |-userModule
| | |-state.js
| | |-mutations.js
| | |-sagas.js
| | |-index.js
- Creating a redux store :
store/index.js
import { module as userModule} from './userModule';
import {module as someOtherModule} from './someOtherModule';import {createStore} from 'redux-box';export default createStore([
userModule,
someOtherModule
])
Finally you need to wrap your root component in the <Provider> </Provider>
which can be imported from redux
; And that's all you would need to setup redux and redux-saga for your application in a modular way.
Understanding the Module
1. state.js
It exports an object specifying the initial state for the parent module. Example: userModule/state.js
export default {
name : '',
email : '',
age : '',
orders : []
}
2. mutations.js
It’s a clutter-free version of the typical reducer we use with redux. it exports an object with multiple functions. Each function name matches certain action type. When an action is dispatched from anywhere in the application, the corresponding method is run and it mutates the state accordingly. Each mutation
accepts two arguments: a copy of the state of it's module and the action that triggers the mutations. Here's an example : userModule/mutations.js
export default {
SET_USER_NAME (state, action){
state.name = action.data;
}
}
Each mutation
receives a copy of the state hence you can directly change the object. Also, you don't need to return the changed state object, redux-box
handles that for you behind the scenes.
3. Sagas
Sagas are used to handle the async operations we might need to perform in our application. Usually to trigger an async process, you would need two sagas: Watcher saga and Worker saga. redux-box
make this process clutter free as well, by providing you with a method called createSagas
. It's optional to use, and you may want to stick to the traditional process of managing sagas, if you need more flexibility. But for most use cases createSagas
can extract away quite a bit of noise. Each Worker saga receives the triggering action as the argument: userModule/Sagas.js
import box from 'redux-box';
import {put} from 'redux-saga';import api from './api'export default box.createSagas({
'GET_ORDERS_LIST.latest' : function* (action){
try{
yield result = api.getOrders(action.data.id)
yield action.resolve('done')
// more about the action.resolve
is covered in the last section (below).
}
catch(err){
action.reject(err)
//...etc
}
}
})
Above code means when GET_ORDERS_LIST action is dispatched anywhere in the app, run the saga mentioned against it. Also, notice the latest
modifier alongside the action name. It's equivalent of takeLatest
from 'redux-saga'. You can also use every
which would produce the same effect as takeEvery
from 'redux-saga'.
4. index.js
It’s the heart of a module which binds all the pieces together. It exports two things : a module and a container (which will be used in any component where you need this module). Here’s how it typically goes: userModule/index.js
import box from 'redux-box';import state from './state'
import mutations from './mutations'
import sagas from './sagas'export const module = {
name : 'user' // it's important to specify a unique name for each module
state,
mutations,
sagas
}export default box.createContainer(module)
Now the magic!
Once you have structured the module and it’s files. You are ready to “code at speed of thought” with redux-box. Let’s say I need the userModule in my App.js
file. Here we go:
App.js
import React, { Component } from 'react';
import UserContainer from './store/userModule'class App extends Component {
render() {
return (
<UserContainer>
{(user)=>(
<h1> {user.name} </h1> <button onClick={()=> user.commit('SET_USER_NAME', 'Roy') } > Change name to Roy </button> <button onClick={()=> {
user.dispatch('GET_ORDERS_LIST')
.then( res => alert('orders updated') )
.catch(err => alert(err.messsage) )
}} >
Get Orders from Api
</button>
)}
</UserContainer>
)
}
}export default App
The above example illustrate the three major jobs of the module-container.
- Firstly, you can directly access the state through the container as shown above.
- Secondly, you can call any
mutation
orsaga
by usingstore.commit
functions, which accepts two arguments : the action name and the data (payload) . The signature for any action returned by thestore.commit
is :
{
type : 'ACTION_NAME'
data : //.. data you attach as the second argument
}
- Thirdly, if you need to do something after your saga is finished, you would use
store.dispatch
instead ofstore.commit
. it will also trigger the mutations and sagas, associated with the underlying action, but additionally it returns aPromise
. Thestore.dispatch
method basically attachedresolve
andreject
keys to the causing action, which you can yield from you sagas, as you could see in the example saga above.
npm: https://www.npmjs.com/package/redux-box
github: https://github.com/anish000kumar/redux-box
If you find the repo useful, please 🌟 star🌟 it and keep building awesome stuff!