preface

Redux, as a react state management tool, has kept many developers away, mainly because of its complicated usage and various components, such as Store and Reducer. This project happens to use Redux for project state management, making the program more elegant, so take this opportunity to summarize.

When do YOU need redux

In fact, in most cases, we don’t need Redux because the actual application scenarios are not complex enough to require Redux.

So, if your UI layer is simple and doesn’t have a lot of interaction, redux is unnecessary and adds complexity.

So when do you need redux? Redux is needed in multi-interaction, multi-data source scenarios, for example:

  1. The way users use it is complex
  2. Users with different identities have different ways of using them
  3. Multiple users can collaborate
  4. Interact heavily with the server, or useWebSocket
  5. ViewData is obtained from multiple sources

In my project, the scenario to consider using Redux looks like this:

Scene: On the poetry page, request the information of the poetry to the background, click a button on the poetry page, it will jump to another page, which also needs the information of the poetry.

A common approach is to request poetry information from the background in the life cycle of two page componentWillMount. However, this approach has the disadvantage of sending multiple requests to the background, resulting in slow page loading and poor performance.

Consider using Redux: when a poetry page requests information about a poem from the background, it stores the information in Redux’s Store, and when it jumps to another page, it retrieves the information directly from the Store. This reduces the number of HTTP requests.

Therefore, we can choose whether or not to use Redux based on our actual situation.

Design idea

Redux’s design philosophy can be summarized as follows:

  1. A Web application is a state machine, and the view corresponds to the state one by one.
  2. All the states are stored in one object.

A one-to-one correspondence between view and state means that a change in state causes a change in view.

All states are stored in an object, which we will refer to later as Store.

Basic concepts and apis

See ruan Yifeng’s blog here.

Store

A Store is a place where you Store data, so you can think of it as a container, and you can only have one Store for the entire application.

Redux provides the createStore function, which generates a Store:

import { createStore } from 'redux';
const store = createStore(fn);
Copy the code

State

The Store object contains all the data. If you want to get data for a time node, you take a snapshot of the Store. This data set of time nodes is called State.

Redux states that one State corresponds to one View. As long as the State is the same, the View is the same.

Action

If the State changes, the View changes. But the user can’t touch State, only View. Therefore, the change of State must be caused by the View. An Action is a notification sent by the View to State indicating that the State is about to change.

The Action must be an object, and the type attribute must be declared to represent the name of the Action. Other attributes can be set freely, details can be found in the community.

Action Creator

An Action is an object, and we must define the Action’s type and data at the beginning. However, data is usually obtained during the execution of the program (such as data obtained from the background) and assigned to the Action. Therefore, we can define a function to generate the Action. This function is called Action Creator.

export const changeAudioInfo = (data) = > ({
  type: GET_AUDIO_INFO,
  data: data, // The entire line can be abbreviated as data. The attribute can be payload or data, which represent the data carried by the action
});
Copy the code

store.dispatch()

Store.dispatch () is the only way a View can issue an Action.

import { createStore } from 'redux';
const store = createStore(fn);

store.dispatch({
  type: 'ADD_TODO'.payload: 'Learn Redux'
});
Copy the code

Store.dispatch takes an Action object as an argument and sends it.

Combined with Action Creator, the code can be written as follows:

store.dispatch(changeAudioInfo(data));
Copy the code

Reducer

When the Store receives the Action, it must give a new State for the View to change. This State calculation process is called Reducer.

Reducer is a function that takes Action and the current State as parameters and returns a new State.

import * as actionTypes from './constants';
import { fromJS } from 'immutable';

const defaultState = fromJS({
  poemInfo: {},
  authorInfo: {},
  audioInfo: {},
  like: false.collect: false});export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.GET_CURRENT_POEM:
      return state.set('poemInfo', action.data);
    case actionTypes.GET_AUTHOR_INFO:
      return state.set('authorInfo', action.data);
    default:
      returnstate; }};Copy the code

The Reducer function does not need to be manually called. The Store. Dispatch method triggers the automatic execution of the Reducer function. Therefore, the Store needs to know the Reducer function, which is passed into the createStore method when the Store is generated.

import { createStore } from 'redux';
const store = createStore(reducer);
Copy the code

This function is called Reducer because it serves as an argument to the reduce method of an array:

const defaultState = 0;

// State is the ACC of the reduce callback, and action is the cur of the reduce callback
const reducer = (state = defaultState, action) = > {
  switch (action.type) {
    case 'ADD':
      return state + action.payload;
    default: 
      returnstate; }};const actions = [
  { type: 'ADD'.payload: 0 },
  { type: 'ADD'.payload: 1 },
  { type: 'ADD'.payload: 2}];const total = actions.reduce(reducer, 0); / / 3
Copy the code

The most important feature of the Reducer function is that it is a pure function. The same input must get the same output. Because Reducer is a pure function, the same State can be guaranteed and the same View must be obtained. Because of this, the Reducer function cannot change State and must return a new object:

// State is an object
function reducer(state, action) {
  return Object.assign({}, state, { thingToChange });
  / / or
  return{... state, ... newState }; }// State is an array
function reducer(state, action) {
  return [...state, newItem];
}

// Using imMUTABLE data streams returns a new state object
export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.GET_CURRENT_POEM:
      return state.set('poemInfo', action.data);
    case actionTypes.GET_AUTHOR_INFO:
      return state.set('authorInfo', action.data);
    default:
      returnstate; }};Copy the code

Reducer of the split

In a real-world application, each Reducer will have its own Reducer, which will be merged globally and passed to the createStore method:

import { combineReducers } from 'redux-immutable';
import { reducer as searchReducer } from '@pages/Search/store/index';
import { reducer as playerReducer } from '@pages/Player/store/index';
import { reducer as poemReducer } from '@pages/Poem/store/index';
import { reducer as recordReducer } from '@pages/Record/store/index';

export default combineReducers({
  search: searchReducer,
  player: playerReducer,
  poem: poemReducer,
  record: recordReducer,
});
Copy the code

What combineReducers() does is generate an overall Reducer function. This function performs the corresponding Reducer according to the State key and merges the result into a large State object.

The global Store data is also retrieved based on the key value:

const mapStateToProps = (state) = > ({
  poemInfo: state.getIn(['poem'.'poemInfo']),
  authorInfo: state.getIn(['poem'.'authorInfo']),
  like: state.getIn(['poem'.'like']),
  collect: state.getIn(['poem'.'collect'])});Copy the code

Redux workflow

First, the user issues Action — > Then the Store automatically calls the Reducer with two parameters: If the State changes, the Store calls a listener (store.subscribe(listener)) to re-render the View.

Middleware and asynchronous operations

If asynchronous operations are involved in the program, we need to use middleware to make the Reducer automatically execute after the asynchronous operation.

The middleware is a function that modifies the store.dispatch method by adding additional capabilities between the actions issued and the Reducer steps.

Usage of middleware

For example, add the output log function:

import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
  reducer,
  applyMiddleware(logger)
);
Copy the code

Two things to note here:

  1. createStoreMethods can take the initial state of the entire application as a parameter, in which case,applyMiddlewareIs the third parameter:
const store = createStore(
  reducer,
  initial_state,
  applyMiddleware(logger)
);
Copy the code
  1. The order of middleware is important
const store = createStore(
  reducer,
  applyMiddleware(thunk, promise, logger)
);
Copy the code

Logger goes last.

ApplyMiddlewares is Redux’s native method of putting all the middleware in an array, executed in sequence.

Story – thunk middleware

Let’s look at an example of making an asynchronous request with Dispatch:

class AsyncApp extends Component {
  componentDidMount() {
    const { dispatch, selectedPost } = this.props
    dispatch(fetchPosts(selectedPost))
  }

// ...
Copy the code

This component performs a dispatch operation in the componentDidMount lifecycle, requesting data fetchPosts(selectedPosts) from the server. FetchPosts here is Action Creator.

FetchPosts’ ‘Action Creator’ looks like this:

const fetchPosts = postTitle= > (dispatch, getState) = > {
  dispatch(requestPosts(postTitle)); // Issue an Action to start the operation
  return fetch(`/some/API/${postTitle}.json`)
    .then(response= > response.json())
    .then(json= > dispatch(receivePosts(postTitle, json))); // Issue another Action to indicate that the operation is complete
  };
};

// Use method 1
store.dispatch(fetchPosts('reactjs'));
// Use method 2
store.dispatch(fetchPosts('reactjs')).then(() = >
  console.log(store.getState())
);
Copy the code

In the above code, fetchPosts is an Action Creator that returns a function. Then fetch is performed inside the function. After receiving the result of the asynchronous operation, an Action is issued through Dispatch to update the value of the variable in the store.

In the above code, there are a few things to note:

  1. fetchPostsReturns a function while normalAction CreatorAn object is returned by default.
  2. The argument to the function returned isdispatchandgetStateThe twoReduxMethod, ordinaryAction CreatorThe parameter isActionThe content of the.
  3. Of the returned functions, emit oneActionIndicates that the operation starts.
  4. After the asynchronous operation is complete, issue anotherActionIndicates that the operation is complete.

We know that the Action is sent by the store.dispatch method. In normal cases, the parameters of the store.dispatch method can only be objects, not functions. Therefore, the middleware Redux-Thunk is used.

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

// Note: this API requires redux@>=3.1.0
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);
Copy the code

Using the Redux-Thunk middleware, modify store.dispatch to accept functions as arguments.

The React – the use of the story

React-redux divides all components into two broad categories: UI components and container components.

UI components

  • Only responsible for the presentation of the UI, with no business logic
  • No state (that is, not usedthis.stateThis variable)
  • All data is defined by parameters (this.props) to provide
  • Without using anyReduxtheAPI

Because it has no state, a UI component is also called a “pure component,” meaning that like a pure function, its values are determined purely by parameters (the same parameters return the same results).

Container components

  • Responsible for managing data and business logic, not UI rendering
  • With internal state
  • useReduxThe API

The UI component is responsible for rendering the UI, and the container component is responsible for managing the data and logic.

If a component has both UI and business logic, what we do is we split it into a container component with the UI component inside. The former is responsible for communicating with the outside world, passing the data to the latter, who renders the view.

React-redux dictates that all UI components are provided by the user, container components are automatically generated by React-Redux, the user takes care of the visual layer, and state management is left to it.

connect()

React-redux provides the connect method for generating container components from UI components. The full API is as follows:

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
Copy the code

The connect method accepts two parameters: mapStateToProps and mapDispatchToProps. They define the business logic of the UI components:

  • mapStateToPropsResponsible for input logic, willstateParameters mapped to UI components (props)
  • mapDispatchToPropsResponsible for output logic that maps user actions to UI componentsAction

Specific usage is as follows:

const mapStateToProps = (state) = > ({
  poemInfo: state.getIn(['poem'.'poemInfo']),
  authorInfo: state.getIn(['poem'.'authorInfo']),
  like: state.getIn(['poem'.'like']),
  collect: state.getIn(['poem'.'collect'])});const mapDispatchToProps = (dispatch) = > {
  return {
    getPoem(poem_id, category) {
      return dispatch(getPoemInfo(poem_id, category)); // dispatch Action Creator
    },
    getAuthor(author_id, category) {
      dispatch(getAuthorInfo(author_id, category));
    },
    getAudio(poem_id, category) {
      return dispatch(getAudioInfo(poem_id, category));
    },
    getDynamic(poem_id, category) {
      dispatch(getDynamicInfo(poem_id, category));
    },
    changeLikeStatus(status) {
      dispatch(changeLike(status));
    },
    changeCollectStatus(status){ dispatch(changeCollect(status)); }}; };export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Poem));
Copy the code

After the mapStateToProps is defined and passed as a parameter to CONNECT, in the component Poem, we can obtain poemInfo, authorInfo, LIKE and collect data from props, which reflects the input logic.

Meanwhile, we can also obtain methods such as getPoem and getAuthor from props in component Poem. When these methods are triggered by events within the component, the corresponding Action can be executed through Dispatch to change the value of variables in store. To further obtain the changed variable value from props.

  • mapStateToPropsIs a function that sets up a function fromstateObject to (UI component)propsObject mapping. It returns an object in which each key-value pair is a mapping.
  • mapDispatchToPropsIt can be a function or an object, usually used as a function, and it returns an object, each key-value pair of which is a mapping, defining what each method emitsAction. (This can be written as a key-value pair or as the code above)

The Provider component

After the connect method generates the container component, it needs to get the state object from the container component to generate the UI component’s parameters.

One solution is to pass a state object as a parameter to the container component. However, this can be cumbersome, especially if container components are at very deep levels, and passing state down from one level to the next is cumbersome.

React-redux provides a Provider component that lets container components get state.

import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'

let store = createStore(todoApp);

render(
  <Provider store={store}>
    <App />
  </Provider>.document.getElementById('root')
Copy the code

In the code above, the Provider wraps a layer around the root component so that all child components of the App get state by default. It works by using the React component’s context property.

The React Redux practice

To use React-Redux in a project, follow these steps:

Installation project dependencies

npm install redux redux-thunk redux-immutable react-redux immutable --save
Copy the code

If the imMUTABLE. Js data structure is used in the project, redux-imMUTABLE needs to be installed. The redux-immutable method needs to be used when the reducer of different modules is merged.

Create the store

Create a store folder in the SRC or app directory and create index.js and reducer.js files in the folder.

//reducer.js
import { combineReducers } from 'redux-immutable';

export default combineReducers ({
Add reducer when developing specific function modules later
});
Copy the code
//index.js
import { createStore, compose, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import reducer from './reducer'
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

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

export default store;
Copy the code

Inject store into the project

import React from 'react'
import { Provider } from 'react-redux'
import store from './store/index'
import routes from './routes/index.js'

function App () {
  return (
    <Provider store={store}>.</Provider>)}export default App;
Copy the code

Create separate stores for components that need to use Redux

// constants.ts
// Define various actions
export const GET_CURRENT_POEM = 'poem/GET_CURRENT_POEM';
export const GET_AUTHOR_INFO = 'poem/GET_AUTHOR_INFO';
export const GET_AUDIO_INFO = 'poem/GET_AUDIO_INFO';
export const GET_LIKE = 'poem/GET_LIKE';
export const GET_COLLECT = 'poem/GET_COLLECT';
Copy the code
// actionCreators.ts
// Define various actions and Action Creator
import {
  GET_CURRENT_POEM,
  GET_AUTHOR_INFO,
  GET_AUDIO_INFO,
  GET_LIKE,
  GET_COLLECT,
} from './constants';
import { fromJS } from 'immutable';
import {
  getPoemDetail,
  getAuthorDetail,
  getAudio,
  getDynamic,
} from '@servers/servers';

export const changePoemInfo = (data) = > ({
  type: GET_CURRENT_POEM,
  data: fromJS(data),
});

export const changeAuthorInfo = (data) = > ({
  type: GET_AUTHOR_INFO,
  data: fromJS(data),
});

export const changeAudioInfo = (data) = > ({
  type: GET_AUDIO_INFO,
  data: fromJS(data),
});

export const changeLike = (data) = > ({
  type: GET_LIKE,
  data: fromJS(data),
});

export const changeCollect = (data) = > ({
  type: GET_COLLECT,
  data: fromJS(data),
});

export const getPoemInfo = (poem_id, category) = > {
  return (dispatch) = > {
    return getPoemDetail(poem_id, category)
      .then((res) = > {
        const curPoem = res[0];

        if (category === '0') {
          curPoem.dynasty = 'S';
        } else if (category === '1') {
          curPoem.dynasty = 'T';
        }

        dispatch(changePoemInfo(curPoem));
      })
      .catch((err) = > {
        console.error(err);
      });
  };
};

export const getAuthorInfo = (author_id, category) = > {
  return (dispatch) = > {
    if(author_id === undefined) {
      dispatch(changeAuthorInfo({}));
    }
    getAuthorDetail(author_id, category)
      .then((res) = > {
        if (res) {
          dispatch(changeAuthorInfo(res[0]));
        }
      })
      .catch((err) = > {
        console.error(err);
      });
  };
};

export const getAudioInfo = (poem_id, category) = > {
  return (dispatch) = > {
    return new Promise((resolve, reject) = > {
      getAudio(poem_id, category)
        .then((data: any) = > {
          if (data.length > 0) {
            dispatch(changeAudioInfo(data[0]));
            resolve(true);
          } else {
            resolve(false);
          }
        })
        .catch((err) = > {
          console.error(err);
        });
    });
  };
};

export const getDynamicInfo = (poem_id, category) = > {
  return (dispatch) = > {
    getDynamic(poem_id, category)
      .then((res: any) = > {
        const { like, collect } = res;
        dispatch(changeLike(like));
        dispatch(changeCollect(collect));
      })
      .catch((err) = > {
        console.error(err);
      });
  };
};

Copy the code
// reducer.ts
// Define defaultState and Reducer
import * as actionTypes from './constants';
import { fromJS } from 'immutable';

const defaultState = fromJS({
  poemInfo: {},
  authorInfo: {},
  audioInfo: {},
  like: false.collect: false});export default (state = defaultState, action) => {
  switch (action.type) {
    case actionTypes.GET_CURRENT_POEM:
      return state.set('poemInfo', action.data);
    case actionTypes.GET_AUTHOR_INFO:
      return state.set('authorInfo', action.data);
    case actionTypes.GET_AUDIO_INFO:
      return state.set('audioInfo', action.data);
    case actionTypes.GET_LIKE:
      return state.set('like', action.data);
    case actionTypes.GET_COLLECT:
      return state.set('collect', action.data);
    default:
      returnstate; }};Copy the code
// index.ts
// Export the Reducer and actionCreators
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';

export { reducer, actionCreators, constants };
Copy the code

After export is exported, the reducer of each component should be imported into the Reducer under the global Store folder, and the reducer of all components should be merged to take effect:

// store/reducer.ts
import { combineReducers } from 'redux-immutable';
import { reducer as searchReducer } from '@pages/Search/store/index';
import { reducer as playerReducer } from '@pages/Player/store/index';
import { reducer as poemReducer } from '@pages/Poem/store/index';
import { reducer as recordReducer } from '@pages/Record/store/index';

export default combineReducers({
  search: searchReducer,
  player: playerReducer,
  poem: poemReducer,
  record: recordReducer,
});
Copy the code

Use in components

.import { connect } from 'react-redux'; .function Poem(props) {...const { poemInfo: poem, authorInfo: author, like, collect } = props; // Get the external (Store) state object passed in from mapStateToProps
  const {
    getPoem,
    getAuthor,
    getAudio,
    getDynamic,
    changeLikeStatus,
    changeCollectStatus,
  } = props; // Get the method passed in from mapDispatchToProps

  let poemInfo = poem ? poem.toJS() : {}; // The object type data requires further toJS operations
  letauthorInfo = author ? author.toJS() : {}; . }const mapStateToProps = (state) = > ({
  poemInfo: state.getIn(['poem'.'poemInfo']),
  authorInfo: state.getIn(['poem'.'authorInfo']),
  like: state.getIn(['poem'.'like']),
  collect: state.getIn(['poem'.'collect'])});const mapDispatchToProps = (dispatch) = > {
  return {
    getPoem(poem_id, category) {
      return dispatch(getPoemInfo(poem_id, category));
    },
    getAuthor(author_id, category) {
      dispatch(getAuthorInfo(author_id, category));
    },
    getAudio(poem_id, category) {
      return dispatch(getAudioInfo(poem_id, category));
    },
    getDynamic(poem_id, category) {
      dispatch(getDynamicInfo(poem_id, category));
    },
    changeLikeStatus(status) {
      dispatch(changeLike(status));
    },
    changeCollectStatus(status){ dispatch(changeCollect(status)); }}; };export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Poem));
Copy the code

Complex scenarios encountered

The previous scenario looked like this: I wanted to get the audio through an asynchronous request, and the audio wanted to be stored in the global Store, so I made the request through a Dispatch operation. At the same time, I want to judge whether the audio exists by whether the data obtained is empty after the request is completed. If it does, it will jump to the Player page; if it does not, the toast prompt will pop up.

The solution I initially came up with was to store a status variable in the store to indicate whether the data is empty or not, and dispatch the status to true when the data is retrieved. But perhaps because dispatch is an asynchronous update, getting the value of the variable passed from props in the component is delayed and does not work.

Because you want to determine if the data is empty after the request is completed, it’s natural to use.then. If you add. Then to a method without rewriting it, an error is reported with store.dispatch(…). Then is not a function.

To sum up, the problem can be summed up in two points: 1. How to convey the message that the requested data is empty; 2. 2. How to solve store.dispatch(…) Then an error

So let’s look at the second question. If there is a.then error, that means that this method is not thenable, so what is.thenable? It’s natural to think of Promise. So we can change the original Action Creator:

export const getAudioInfo = (poem_id, category) = > {
  return (dispatch) = > {
      getAudio(poem_id, category)
        .then((data) = > {
          if (data.length > 0) {
            dispatch(changeAudioStatus(true));
            dispatch(changeAudioInfo(data[0]));
            resolve(true);
          } else {
            dispatch(changeAudioStatus(false));
            resolve(false);
          }
        })
        .catch((err) = > {
          console.error(err);
        });
  };
};
Copy the code

Rewrite:

export const getAudioInfo = (poem_id, category) = > {
  return (dispatch) = > {
    return new Promise((resolve, reject) = > {
      getAudio(poem_id, category)
        .then((data) = > {
          if (data.length > 0) {
            dispatch(changeAudioStatus(true));
            dispatch(changeAudioInfo(data[0]));
          } else {
            dispatch(changeAudioStatus(false));
          }
        })
        .catch((err) = > {
          console.error(err);
        });
    });
  };
};
Copy the code

In mapDispatchToProps, replace dispatch(XXX) with return dispatch(XXX) :

const mapDispatchToProps = (dispatch) = > {
  return {
    getAudio(poem_id, category) {
      returndispatch(getAudioInfo(poem_id, category)); }}; };Copy the code

When you write this, you won’t report the. Then error. (Reference: store.dispatch(…) . Then is not a function)

Let’s go back to the first question. We cannot currently pass information about the existence of the data by storing the status variable in the store, because there is a delay in the dispatch asynchronous update. In promises, we use resolve and Reject to pass information, which can be retrieved in a later. Then callback.

Resolve (true), reject(false); resolve(false)

export const getAudioInfo = (poem_id, category) = > {
  return (dispatch) = > {
    return new Promise((resolve, reject) = > {
      getAudio(poem_id, category)
        .then((data: any) = > {
          if (data.length > 0) {
            dispatch(changeAudioInfo(data[0]));
            resolve(true); // Data exists
          } else {
            resolve(false); // Data does not exist
          }
        })
        .catch((err) = > {
          console.error(err);
        });
    });
  };
};
Copy the code

The front end determines whether to jump to the page or pop up a prompt by calling the status parameter passed in. Then:

const handleListen = () = > {
    getAudio(id, category).then((status) = > {
      if (status) {
        Taro.navigateTo({
          url: '/pages/Player/index'}); }else {
        setShowToast(true); }}); };Copy the code

conclusion

React-Redux is a React state management tool. It is designed to solve the problem of state sharing in large projects. Without react-redux, components can only share state through routing or context, which is cumbersome. With React-Redux, variables that might be used in multiple components can be stored in a store, which components can use from the Store, making the overall structure of the code more elegant and readable.