Flow Control in Redux

@EmaSuriano

Written by:

Summary

  • What is middleware?
    • Basic Structure
    • Next vs Dispatch
    • Connecting to Redux
    • Environment Configuration
  • Advantages
  • Building Middlewares
    • MeasureMiddleware
    • APIMiddleware
    • CustomMiddleware (Flow Control)
  • MOTY's
    • Redux-thunk
    • Redux-sagas

What is Middleware?

Redux Doc.

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

Simple Word

Allow us to add code that will run before action is passed to the reducer.

Basic Structure

const myMiddleware = ({ getState, dispatch }) => next => action => {
    /* 
        before reducers have run
    */

    next(action); // will propagate the action down the middleware chain

    /* 
        after reducers have run
    */
}; 

Next

Will pass the action alogn the middleware chain (from the current) down to the reducer.

Dispatch

Will start the action flow from the beggining (the first middleware in the), so it will eventualyy reach the current one as well.

A

B

C

next()

Red

dispatch()
action

Middleware Chain

Connecting to Redux

import { createStore, applyMiddleware } from 'redux';
import reducer from 'reducers/root';
import thunk from 'redux-thunk';
import measureMiddleware from 'middleware/measure';

const middlewareChain = applyMiddleware(measureMiddleware, thunk);
const store = createStore(reducer, middlewareChain);

By calling applyMiddleware and passing our middlewares, we are defining the middleware chain for the application. The order is defined by the middleware's order.

Environment Configuration

const middleware = [apiMiddleware]; //middlewares that will run everywhere

if (development) { 
    middleware.push(measureMiddleware); // only on dev
} else {
    middleware.push(analyticsMiddleware);
}

const store = createStore(reducer, applyMiddleware(...middleware)); 

We have the ability to defined which middlewares will run inside our differents enviroment. For example, we want to use the measureMiddleware only in Dev.

Advantages of using them

  • Abstraction of actionCreators
  • Responsibilities Splitting
  • Flow/Side effects Control
  • Testeability

Building a Middleware

Measure Middleware

const measureMiddleware = () => next => action => { 
    console.time(action.type);
    next(action);
    console.timeEnd(action.type);
};

export default measureMiddleware; 

This middleware allow us to potentially catch poorly performing reducer implementations. We start the timing before running an action, then we tell the browser to print the timing after the action is done.

API Middleware

Our goal is to create a generic middleware that can serve any API request and requires only the information passed to it in an action.

To use our middleware we need to craft a special action object for it: 

API Middleware

In this case, this middleware will manage any API request and requires only the information passed to it in an action.

To use our middleware we need to craft some special actions object for it:

const fetchUser = (id, cancelableId) => ({
    type: API,
    url: `user/${id}`,
    next: SET_USER,
    cancelableId,
}); 


const cancelAPI = (id) => ({
    type: CANCEL_API,
    id,
}); 

Dynamic Action Types

With this approach we can use the sme action type to handle three cases for async actions

const asyncActionType = (type) => ({
    PENDING: `${type}_PENDING`,
    SUCCESS: `${type}_SUCCESS`,
    ERROR: `${type}_ERROR`,
});

export const LOGIN = asyncActionType('LOGIN');
export const FETCH_RECIPES = asyncActionType('FETCH_RECIPES');
const canceled = {};

const apiMiddleware = ({ dispatch }) => (next) => (action) => {

  const handleResponse = (data) => {
    if (action.cancelable && canceled[action.cancelable]) {
      return;
    }

    dispatch({ type: action.payload.next.SUCCESS, payload: data })
  };

  if (action.type === API_REQUEST) {
    fetch(action.payload.url)
      .then((response) => response.json())
      .then(handleResponse);

    dispatch({ type: action.payload.next.PENDING });
  }

  if (action.type === CANCEL_API_REQUEST) {
    canceled[action.id] = true;
    setTimeout(() => delete canceled[action.id], 5000);
  }

  return next(action);
};

Custom Middleware for Flow Control

const MY_ACTION = {
    type: 'MY_ACTION',
};

const doSomething = (dispatch) => {
    dispatch(DO_SOMETHING);
    dispatch(MY_ACTION);
}

const doMoreStuff = (dispatch) => {
    dispatch(DO_MORE_STUFF);
    dispatch(MY_ACTION);
}

const doWhateverYouLike= (dispatch) => {
    dispatch(DO_WHATEVER_YOU_LIKE);
    dispatch(MY_ACTION);
}

In this case, we're going to create a Middleware to help us managing side-effects on out application.

Let's suppose that we have an action (MY_ACTION) that needs to be dispatch after a series of actions.

const MY_ACTION = {
    type: 'MY_ACTION',
};

const doSomething = (dispatch) => {
    dispatch(DO_SOMETHING);
}

const doMoreStuff = (dispatch) => {
    dispatch(DO_MORE_STUFF);
}

const doWhateverYouLike= (dispatch) => {
    dispatch(DO_WHATEVER_YOU_LIKE);
}


const customMiddleware = ({ getState, dispatch }) => next => action => {
	switch(action.type) {
		case DO_SOMETHING:
		case DO_MORE_STUFF:
		case DO_WHATEVER_YOU_LIKE:
			dispatch(MY_ACTION);
		default: break;
	}

	next(action);
};

Middlewares of the Years

most used middlewares

Redux-thunk

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

Allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met.

Inside Redux-thunk

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

Redux-saga

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

export default mySaga;

Aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.

Connecting with Redux

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)

// then run the saga
sagaMiddleware.run(mySaga)

// render the application

Questions?

Thanks!

FlowControlRedux

By Emanuel Suriano

FlowControlRedux

Introduction to middlewares in Redux, and some examples of them created by the community.

  • 308
Loading comments...

More from Emanuel Suriano