Simplify Redux Actions with Middleware
Comparing Flux with Redux
Working with React, we compared flux with redux, our team decided to use redux. The devtools redux offers was icing on the cake. Diving in, we wanted a central location for all of our API calls. The API middleware would need to handle dispatching a pre-request, successful request, and failures. To handle this, we set our actions in the following format
Actions
import DT_API_CALL from '../middleware/api'; const USER_REQUEST = 'USER_REQUEST'; const USER_SUCCESS = 'USER_SUCCESS'; export function fetchUser(userId) { return { [DT_API_CALL]: { types: [USER_REQUEST, USER_SUCCESS], endpoint: '/user/${userId}', method: 'get' } }; } export function saveUser(user) { return { [DT_API_CALL]: { types: [USER_REQUEST, USER_SUCCESS], endpoint: '/user/update', method: 'put', data: user } }; }
Configure middleware
Configuring our middleware looks something like:
export default store => next => action => { const apiCall = action[DT_API_CALL]; if (typeof apiCall === 'undefined') { return next(action); } ... return callApi(apiCall).then( response => next(actionWith({payload: response, type: successAction})), errors => ({errors, type: ServerActions.SERVER_FAILURE}));
I’ve removed some code for brevity, the main point is displayed. We make an axios API call and handle the response in the middleware dispatching to the correct action handler, or reducer.
Configure store
To configure our store so that it knows about our middleware we simply do the following:
import api from '../middleware/api'; ... const finalCreateStore = compose( applyMiddleware(thunk, api))(createStore);
Inside our compose we just need to add our API middleware. Now our API will be called after our actions and before our reducers.
Containers
Now that our API is configured we can use it to fetch a user.
import UserActions from '../actions/UserActions'; ... const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators(UserActions, dispatch) }); class UserDisplay extends React.Component { ... componentWillMount() { this.props.actions.fetchUser(this.props.userId); // we can now use this.props.user.currentUser } ... } export default connect(state => state, mapDispatchToProps)(UserDisplay);
All we have left is our reducer.
Reducer
The reducer is where we handle updating the data in our state. The reducer is pretty simple to setup.
import * as UserActions from '../actions/UserActions'; export default function user(state = { currentUser: null }, action) { switch (action.type) { case UserActions.USER_SUCCESS: return Object.assign({}, state, { currentUser: action.payload }); default: return state; } }
For our server errors, we would create a serverError reducer and pull in the ServerActions. Then handle setting errors on a SERVER_FAIL action dispatched from our middleware.
Conclusion
Working with state on the client can become difficult, however, redux greatly simplifies this. Don’t forget to checkout the devtools too, they are a huge help in identifying state changes and how the UI is responding.