Redux and its middleware: redux-thunk, redux-actions, redux-promise, redux-sage

Keywords: Javascript React axios github less

Preface

Here we will talk about the application of Redux in React. We will talk about the functions of Redux, react-redux, redux-thunk, redux-actions, redux-promise, redux-sage and the problems they solve.
Because I don't want to stretch the space too long, I don't have too many source code analysis and grammar explanations. It's as simple as it can be.

Redux

First look at a picture of Redux on Baidu Encyclopedia:

This is Redux's introduction on Github: Redux is a predictable state container for js programs.

The first thing we need to understand here is what is predictability? What is a state container?

What is state? It's actually a variable, a dialog box showing or hiding variables, a variable of how much a cup of milk tea costs.

So this state container is actually a variable that holds these variables.

You create a global variable called Store and store the variables in the code that control each state. Now Store is called a state container.

What is predictability?

When you operate the Store, you always set values in the way of Store.price. This way of manipulating data is very primitive. For complex systems, you never know what happens in the process of running the program.

So now we can make changes by sending an Action, and Store will use Reducer to process the data transferred by the Action when it receives the Action, and finally apply it to Store.

This approach is undoubtedly more cumbersome than the Store.price approach to modify, but the advantage of this approach is that each Action can be written in a log, can record changes in various states, which is predictable.

So if your program is simple, you don't need to use Redux at all.

Look at the example code for Redux:

actionTypes.js:

export const CHANGE_BTN_TEXT = 'CHANGE_BTN_TEXT';

actions.js:

import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

reducers.js:

import * as T from './actionTypes';

const initialState = {
  btnText: 'I am the button.',
};

const pageMainReducer = (state = initialState, action) => {
  switch (action.type) {
    case T.CHANGE_BTN_TEXT:
      return {
        ...state,
        btnText: action.payload
      };
    default:
      return state;
  }
};

export default pageMainReducer;

index.js

import { createStore } from 'redux';
import reducer from './reducers';
import { changeBtnText } from './actions';

const store = createStore(reducer);
// Start listening and print out the current status each time the state is updated
const unsubscribe = store.subscribe(() => {
  console.info(store.getState());
});
// send message
store.dispatch(changeBtnText('Click the button'));
// Stop listening for state updates
unsubscribe();

There is no explanation for grammatical function here. There are too many such materials on the Internet.

Combination of Redux and React: react-redux

Redux is a predictable state container, and a library that builds UI s like React is two separate things.

To apply Redux to React, it is obvious that the stages of action, reducer and dispatch need not be changed. The only consideration is how the state in Redux needs to be passed to the react component.

Simply, you need to use store.getState to get the current state each time you update the data and pass it to the component.

So the question arises, how do you get each component to store?

Of course, store is passed as a value to the root component, and then store is downloaded one by one so that each component can get the store value.

But this is too cumbersome. Does each component need to write a logic to pass store? To solve this problem, we need to use the context play of React, by placing the store in the context of the root component, and then getting the store through the context in the sub-component.

The main idea of react-redux is the same. Stores are put into context by nested component Provider, and store s are hidden by connect ing this high-level component, so that we don't need to write a lot of code every time we operate context.

Then we will give a demonstration code of react-redux based on the previous Redux sample code, where action and reduce remain unchanged. First we add a component, PageMain:

const PageMain = (props) => {
  return (
    <div>
      <button onClick={() => {
        props.changeText('The button was clicked.');
      }}
      >
        {props.btnText}
      </button>
    </div>
  );
};
// Mapping store.getState() data to PageMain
const mapStateToProps = (state) => {
  return {
    btnText: state.pageMain.btnText,
  };
};
// Mapping uses store.dispatch functions to PageMain
const mapDispatchToProps = (dispatch) => {
  return {
    changeText: (text) => {
      dispatch(changeBtnText(text));
    }
  };
};

// This place can also be abbreviated, react-redux will automatically do processing
const mapDispatchToProps = {
  changeText: changeBtnText
};

export default connect(mapStateToProps, mapDispatchToProps)(PageMain);

Notice the state.pageMain.btnText above, which I named the original reducer after merging multiple reducers with redux combineReducers.

Its code is as follows:

import { combineReducers } from 'redux';
import pageMain from './components/pageMain/reducers';

const reducer = combineReducers({
  pageMain
});

export default reducer;

Then modify index.js:

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import ReactDOM from 'react-dom';
import reducer from './reducers';
import PageMain from './components/pageMain';

const store = createStore(reducer);

const App = () => (
  <Provider store={store}>
    <PageMain />
  </Provider>
);

ReactDOM.render(<App />, document.getElementById('app'));

Redux Middleware

Previously, we talked about Redux as a predictable state container, which predicts that every modification of data can be processed and recorded accordingly.

If we need to record the changes every time we modify the data, we can add a console.info record the changes before each dispatch.

But this is too cumbersome, so we can modify store.dispatch directly:

let next = store.dispatch
store.dispatch = (action)=> {
  console.info('The amendments are as follows:', action)
  next(action)
}

The same functionality exists in Redux, which is apply Middleware. Literally translated as "application middleware", its role is to transform dispatch function, basically the same as the above play.

Here's a demo code:

import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';

const store = createStore(reducer, applyMiddleware(curStore => next => action => {
  console.info(curStore.getState(), action);
  return next(action);
}));

It looks strange, but it's not difficult to understand. With this method of returning functions, store s and action s can be processed within the applyMiddleware and when we use them, and the application of next exists for the use of multiple middleware.

Normally, we don't need to write middleware by ourselves. For example, log records already have a mature middleware: redux-logger. Here's a simple example:

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
import reducer from './reducers';

const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

This allows you to log all action s and their state s before and after they are sent, and we can see what happens when the code actually runs.

redux-thunk: Handling asynchronous action s

In the code above, we clicked the button and directly changed the text of the button, which is a fixed value.

actions.js:

import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

But in our actual production process, many cases need to request the server to get the data and then modify, this process is an asynchronous process. Or you need setTimeout to do something.

We can modify this part as follows:

const mapDispatchToProps = (dispatch) => {
  return {
    changeText: (text) => {
      dispatch(changeBtnText('Loading in progress'));
      axios.get('http://test.com').then(() => {
        dispatch(changeBtnText('Loaded'));
      }).catch(() => {
        dispatch(changeBtnText('Incorrect loading'));
      });
    }
  };
};

In fact, we don't know how much such code we need to deal with every day.

But the problem is that asynchronous operation has a lot more determinants than synchronous operation. For example, when we show that we are loading, we may need to do asynchronous operation A first, while the process of requesting background is very fast, which results in loading first, and then operation A is finished, and then show loading.

So the above method does not satisfy this situation.

At this point, we need to get the current state through store.getState to determine whether the display is loading or the display is finished.

This process can not be placed in map Dispatch ToProps, but in middleware, because the middleware can get the store.

First of all, you need to apply react-thunk when you create a store, that is, react-thunk.

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';

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

Its source code is super simple:

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;

From this, we can see that it strengthens the function of dispatch. Before dispatch is an action, we can judge whether the action is a function or not. If it is a function, then we can execute the function.

So it's easy for us to use, and we'll modify actions.js at this point.

import axios from 'axios';
import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

export const changeBtnTextAsync = (text) => {
  return (dispatch, getState) => {
    if (!getState().isLoading) {
      dispatch(changeBtnText('Loading in progress'));
    }
    axios.get(`http://test.com/${text}`).then(() => {
      if (getState().isLoading) {
        dispatch(changeBtnText('Loaded'));
      }
    }).catch(() => {
      dispatch(changeBtnText('Incorrect loading'));
    });
  };
};

The original mapDispatchToProps play the same way as synchronization:

const mapDispatchToProps = (dispatch) => {
  return {
    changeText: (text) => {
      dispatch(changeBtnTextAsync(text));
    }
  };
};

By redux-thunk, we can simply perform asynchronous operations, and we can get the state values of each asynchronous operation period.

redux-actions: simplifying the use of redux

Redux is useful, but there are some duplicates in it, so redux-actions are available to simplify those duplicates.

This part of the simplification work mainly focuses on the construction of action and the processing of reducers.

Let's look at the original actions first.

import axios from 'axios';
import * as T from './actionTypes';

export const changeBtnText = (text) => {
  return {
    type: T.CHANGE_BTN_TEXT,
    payload: text
  };
};

export const changeBtnTextAsync = () => {
  return (dispatch, getState) => {
    if (!getState().isLoading) {
      dispatch(changeBtnText('Loading in progress'));
    }
    axios.get('http://test.com').then(() => {
      if (getState().isLoading) {
        dispatch(changeBtnText('Loaded'));
      }
    }).catch(() => {
      dispatch(changeBtnText('Incorrect loading'));
    });
  };
};

Then look at the revised:

import axios from 'axios';
import * as T from './actionTypes';
import { createAction } from 'redux-actions';

export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);

export const changeBtnTextAsync = () => {
  return (dispatch, getState) => {
    if (!getState().isLoading) {
      dispatch(changeBtnText('Loading in progress'));
    }
    axios.get('http://test.com').then(() => {
      if (getState().isLoading) {
        dispatch(changeBtnText('Loaded'));
      }
    }).catch(() => {
      dispatch(changeBtnText('Incorrect loading'));
    });
  };
};

When this code replaces some of the above code, the result of the program still remains unchanged, that is to say, the createAction simply encapsulates the above code.

Notice here that asynchronous action s do not use createAction, because the createAction returns an object, not a function, which causes redux-thunk code to fail.

It's also possible to create multiple action s at the same time using the function createActions, but reasonably, this grammar is strange, just use createAction.

Similarly, redux-actions deal with the part of reducer, such as handleAction and handelActions.

Let's first look at the original reducers

import * as T from './actionTypes';

const initialState = {
  btnText: 'I am the button.',
};

const pageMainReducer = (state = initialState, action) => {
  switch (action.type) {
    case T.CHANGE_BTN_TEXT:
      return {
        ...state,
        btnText: action.payload
      };
    default:
      return state;
  }
};

export default pageMainReducer;

Then use handleActions to process

import { handleActions } from 'redux-actions';
import * as T from './actionTypes';

const initialState = {
  btnText: 'I am the button.',
};

const pageMainReducer = handleActions({
  [T.CHANGE_BTN_TEXT]: {
    next(state, action) {
      return {
        ...state,
        btnText: action.payload,
      };
    },
    throw(state) {
      return state;
    },
  },
}, initialState);

export default pageMainReducer;

Here handleActions can add exception handling and help with initial values.

Note that both createAction and handleAction simply encapsulate the code, and they can be used separately, not necessarily using handleAction when using createAction.

redux-promise: a good friend of redux-actions, easy to create and handle asynchronous actions

Remember that when we used redux-actions createAction above, we were unable to handle asynchronous actions.

Because we use createAction to return an object, not a function, the redux-thunk code doesn't work.

Now we will use redux-promise to deal with this kind of situation.

Let's take a look at the previous example of using createAction:

export const changeBtnText = createAction(T.CHANGE_BTN_TEXT, text => text);

Now let's add redux-promise middleware:

import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';

const store = createStore(reducer, applyMiddleware(thunk, createLogger, promiseMiddleware));

Then the asynchronous action is processed:

export const changeBtnTextAsync = createAction(T.CHANGE_BTN_TEXT_ASYNC, (text) => {
  return axios.get(`http://test.com/${text}`);
});

You can see that we are returning a Promise object. (The get method of axios results in a Promise object.)

We remember the redux-thunk middleware, which decides whether action is a function, and if so, it executes it. uuuuuuuuuuu

In our redux-promise middleware, he will judge if action is not similar in dispatch

{
  type: '',
  payload:  ''
}

Such a structure, i.e. FSA Then you decide whether it's a promise object or not, and if so, you execute the action.then method.

Obviously, the result of our createAction is FSA, so we'll go to the next branch, which will determine whether action.payload is a promise object or not. Yes, that's it.

action.payload
  .then(result => dispatch({ ...action, payload: result }))
  .catch(error => {
    dispatch({ ...action, payload: error, error: true });
    return Promise.reject(error);
  })

That is to say, our code will eventually change to:

axios.get(`http://test.com/${text}`)
  .then(result => dispatch({ ...action, payload: result }))
  .catch(error => {
    dispatch({ ...action, payload: error, error: true });
    return Promise.reject(error);
  })

The middleware code is also very simple, a total of 19 lines, you can see directly on github.

redux-sage: controller and more elegant asynchronous processing

Our asynchronous processing uses redux-thunk + redux-actions + redux-promise, which is actually quite useful.

But with the emergence of Generator in ES6, it has been found that using Generator to handle asynchrony can be simpler.

And redux-sage uses Generator to handle asynchrony.

The following knowledge is based on Generator. If you don't know much about it, you can get a brief understanding of it. It takes about 2 minutes. It's not difficult.

The redux-sage document does not refer to itself as a tool for dealing with asynchronism, but rather as a tool for dealing with side effects, where you can understand the marginal effects as the external operations of the program, such as the request back end, such as the operation files.

redux-sage is also a redux middleware, its positioning is through centralized control action, playing a similar effect to the controller in MVC.

At the same time, its grammar makes complex asynchronous operations less likely to have many then situations like promise, which makes it easier to perform various kinds of tests.

This thing has its advantages, as well as its disadvantages, that is, it is more complex and has a certain learning cost.

And personally, I'm not used to Generator, and I think Promise or await is better.

Here's how to use it. After all, there are many frameworks that use it.

Applying this middleware is no different from our other middleware:

import React from 'react';
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import createSagaMiddleware from 'redux-saga';
import {watchDelayChangeBtnText} from './sagas';
import reducer from './reducers';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(reducer, applyMiddleware(promiseMiddleware, sagaMiddleware));

sagaMiddleware.run(watchDelayChangeBtnText);

After the sage middleware is created, the middleware is then connected to the store. Finally, the Generator returned by sages.js needs to be run with the middleware to monitor each action.

Now let's give the code for sages.js:

import { delay } from 'redux-saga';
import { put, call, takeEvery } from 'redux-saga/effects';
import * as T from './components/pageMain/actionTypes';
import { changeBtnText } from './components/pageMain/actions';

const consoleMsg = (msg) => {
  console.info(msg);
};
/**
 * Functions to deal with editing effects
 */
export function* delayChangeBtnText() {
  yield delay(1000);
  yield put(changeBtnText('123'));
  yield call(consoleMsg, 'Completion of change');
}
/**
 * Functions for Monitoring Action
 */
export function* watchDelayChangeBtnText() {
  yield takeEvery(T.WATCH_CHANGE_BTN_TEXT, delayChangeBtnText);
}

In redux-sage, there is a class of functions used to deal with marginal effects, such as put and call, whose purpose is to simplify operations.

For example, put is equivalent to dispatch of redux, and call is equivalent to calling function. (Refer to the example in the code above)

Another kind of function is similar to takeEvery, whose function is to intercept action and make corresponding processing just like ordinary redux middleware.

For example, the above code intercepts the action of the type T.WATCH_CHANGE_BTN_TEXT, and then calls delayChangeBtnText.

Then you can look back at our previous code, there is such a line of code:

sagaMiddleware.run(watchDelayChangeBtnText);

In fact, after introducing the monitor generator, it runs the monitor generator.

In this way, when the dispatch type in the code is T.WATCH_CHANGE_BTN_TEXT's action, it will be intercepted and processed accordingly.

Of course, some people here may ask questions. Does every asynchrony have to be written like this? Doesn't it take run many times?

Of course it's not like this. We can write this in sage:

export default function* rootSaga() {
  yield [
    watchDelayChangeBtnText(),
    watchOtherAction()
  ]
}

We just need to write in this format, put the generator for monitoring action s like watchDelayChangeBtnText in the array of the code above, and return it as a generator.

Now you just need to refer to the rootSaga, and then run the rootSaga.

In the future, if you want to monitor more action s, you only need to add a new monitor generator in sages.js.

In this way, we can make sages.js into a controller like MVC, which can be used to deal with a variety of action s, complex asynchronous operations and marginal effects.

However, it is important to distinguish the actions used in sages.js from the actions used in real functions, such as adding a watch keyword, in order to avoid code confusion due to business complexity.

summary

In general:

  • redux is a predictable state container.
  • react-redux combines store and react to make data presentation and modification easier for react projects
  • redux middleware deals with action before dispatch action
  • redux-thunk for asynchronous operation
  • redux-actions for simplifying redux operations
  • redux-promise can be used in conjunction with redux-actions to process Promise objects, making asynchronous operations easier
  • redux-sage can be used as a controller to centralize the marginal utility and make the asynchronous operation more elegant.

OK, although I don't want to write so much, I still write a lot.

If you think it's helpful to you, please give me a compliment.

Posted by yobo on Sun, 16 Dec 2018 17:42:03 -0800