Redux is an implementation of the Flux concept.

The Flux concept can be briefly understood by analogy with the MVC pattern. In MVC mode, the user request reaches the Controller first, the Controller calls the Model to get the data, and then gives the data to the View. According to this mode, MVC should also be a one-way data flow of Controller -> Model -> View. However, in practical application, For a variety of reasons, the view will often operate the Model directly. As the application evolves and the logic becomes more and more complex, the relationship between the View and the Model will become intricate and difficult to maintain. Having the View talk to the Model directly in MVC is a disaster. The Flux concept can simply be seen as adding tighter data flow constraints to MVC.

The Flux framework was introduced by Facebook along with React, but After Dan Abramov created Redux, Redux replaced Flux.

The basic principle of

The basic principle of Flux is “one-way data flow”, on which Redux emphasizes three basic principles:

  • Single Source of Truth. The application state data should only be stored in a unique Store, which is a tree structure. Each component usually uses only part of the data in the tree object.

  • State is read-only. You cannot modify the State of the store directly. You must distribute action objects to modify the State of the store.

  • Data changes can only be made with pure functions.

This pure function is Reducer, and Dan Abramov said that Redux’s name means Reducer+Flux. Reducer is a Reducer function supported by many languages. Here is how to use the reduce function of the array in JS:

[1,2,3,4]. Reduce (function reducer(job + group); }, 0)Copy the code

The first parameter of the reduce function is reducer. This function makes all elements in the array “reduce” in turn, calls reducer for each element, and completes the reducer function for all elements.

In Redux, the reducer function signature is:

reducer(state, action)
Copy the code

It returns a new state object based on the state and action values. Reducer is a pure function that only calculates the state and does not store the state.

The use of the story

The template created using create-react-app does not contain redux dependencies, so you need to run NPM install redux first.

The standard one-way data flow in MVC is Controller ->model-> View. Accordingly, after React and Redux cooperate, to trigger the update of view, an action needs to be issued and the reducer updates the store state according to the action. Finally, let the View update based on the latest data in the Store.

Action

Redux applications traditionally split the action type and action constructor into two file definitions that look something like this: actiontypes.js

export const INCREMENT="increment";
export const DECREMENT="decrement";
Copy the code

Each action constructor in actions.js returns an action object

import * as ActionTypes from './ActionTypes.js';

export const increment = (counterCaption) => {
  return {
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  }
};

export const decrement = (counterCaption) => {
  return {
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  }
};
Copy the code

Store

Store. Js, for example

import { createStore } from 'redux';
import reducer from './Reducer.js';

const initValues = {
  'First': 0,
  'Second': 10,
  'Third': 20
}

const store = createStore(reducer, initValues);

export default store;
Copy the code

To create a store, call the createStore function provided by the Redux library:

  • The first parameter is reducer, which is responsible for updating the update status
  • The second parameter is the initial value of the state
  • There is also a third parameter called Store Enhancer, more on that later

Determining Store state is the key to designing Redux applications. The main principles are: avoid redundant data;

Reducer

Reducer, js, for example

import * as ActionTypes from './ActionTypes.js'; export default (state, action) => { const { counterCaption } = action; switch (action.type) { case ActionTypes.INCREMENT: return { ... state, [counterCaption]: state[counterCaption] + 1 }; case ActionTypes.DECREMENT: return { ... state, [counterCaption]: state[counterCaption] - 1 }; default: return state; }}Copy the code

The main structure of Reducer is if-else or switch statements, which execute corresponding reduce operations according to action.type. . State is the syntax of the extended operator. The state field is extended and assigned to a new object, and then the corresponding field is modified according to counterCaption.

const newState = Object.assign({}, state);
newState[counterCaption]++;
return newState;
Copy the code

The Spread operator syntax is not an ES6 syntax, but is widely used because of its syntax simplicity, and Babel takes care of compatibility issues.

Reducer is a pure function that does not modify the original state but instead operates on the state of the new copy.

View

View code example:

import { Component } from 'react'; import PropTypes from 'prop-types'; import store from '.. /Store.js'; import * as Actions from '.. /Actions.js' class Counter extends Component { constructor(props) { super(props); /* bind methods*/ this.state = this.getOwnState(); } getOwnState() { return { value: store.getState()[this.props.caption] } } onChange() { this.setState(this.getOwnState()); } onClickIncrementButton() { store.dispatch(Actions.increment(this.props.caption)); } onClickDecrementButton() { store.dispatch(Actions.decrement(this.props.caption)); } componentDidMount() { store.subscribe(this.onChange); } componentWillUnmount() { store.unsubscribe(this.onChange); } render() { const { caption } = this.props; const value = this.state.value; return ( <div> <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button> <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button> <span>{caption} count:{value}</span> </div> ); }}Copy the code
  • store.getState()Get all the states stored in the store. The current component only needs to get its own state information.
  • Subscribe to store state changes in componentDidMount;

When the store state changes, onChange is triggered. In this case, this.setState is called to trigger view updates.

  • In addition, unlisten for store state changes in componentWillUnmount;
  • When a button is clicked, an action is distributed through store.dispatch, which is generated by an action constructor in actions.js.

Container components and presentation components

As you can see from the Redux sample code, the React component basically does two things:

  • Responsibilities related to store:
    • Read and listen for Store state changes;
    • When the Store state changes, update the component state and drive the component to re-render;
    • When the Store status needs to be updated, the action object is distributed.
  • Render the user interface based on the current props and state.

Consider splitting the component in two:

  • Container component: Responsible for store
  • Presentation components: Focus only on page rendering

The presentation component has no state and is a pure function that only needs to be rendered according to props.

Component Context

If each component is split into a container component and a presentation component, then each container component needs to be imported into a Store because the container component needs to interact with the Store. It is best to import only the top component once, and then the child components can be used. Instead, use the Context provided by React.

To use the Context requires coordination between the upper and lower components, starting with the top-level component. Create a Provider component here as a generic Context Provider

import { Component } from 'react';
import PropTypes from 'prop-types';

class Provider extends Component {
  getChildContext() {
    return {
      store: this.props.store
    };
  }

  render() {
    return this.props.children;
  }
}

Provider.childContextTypes = {
  store: PropTypes.object
}

Provider.propTypes = {
  store: PropTypes.object.isRequired
}

export default Provider;
Copy the code

This component is used as a parent component:

··· import store from './ store.js '; import Provider from './Provider.js'; ReactDOM.render( <Provider store={store}> <ControlPanel /> </Provider>, document.getElementById('root') );Copy the code

The top Provider of components, by the Provider. ChildContextTypes to claim to support the context, and to provide getChildContext function to return on behalf of the context object, The value stored in the Context object is actually the store imported by index.js, which is passed to the Provider as props. This allows you to import store only once in index.js. The Provider component also renders the child component through this.props. Children.

Then its descendant components, as long as they declare that they need the context, can access the common environment object through this.context. Something like this:

SummaryContainer.contextTypes = {
  store: PropTypes.object
}
Copy the code

In addition, the constructor also passes the context:

constructor(props, context) { super(props, context); ...}Copy the code

This can be further simplified as:

constructor() {
  super(...arguments);
  ···
}
Copy the code

React-Redux

The above process improves the React application in two ways:

  • Split components into container components and presentation components
  • All components access the store through Context

In fact, using the React-Redux library can do most of the work above, but you can see how it works by following the step-by-step process above.

With the introduction of React-Redux, the code is much cleaner than before. Import Provider from react-redux:

import {Provider} from 'react-redux';
Copy the code

In the child component, the container component is also created by react-Redux, so we just need to write the presentation component and define mapStateToProps and mapDispatchToProps. The getOwnState method was defined on the container component to convert the state on the store to the props for presenting the component

getOwnState() {
  return {
    value: this.context.store.getState()[this.props.caption]
  }
}
Copy the code

MapStateToProps does the same thing:

function mapStateToProps(state, ownProps) {
  return {
    value: state[ownProps.caption]
  }
}
Copy the code

The container component also defines functions that click to distribute the action,

onClickIncrementButton() {
  this.context.store.dispatch(Actions.increment(this.props.caption));
}

onClickDecrementButton() {
  this.context.store.dispatch(Actions.decrement(this.props.caption));
}
Copy the code

The same is true of the mapDispatchToProps, which passes dispatches to the props of the presentation component for active firing:

function mapDispatchToProps(dispatch, ownProps) { return { onIncrement: () => { dispatch(Actions.increment(ownProps.caption)); }, onDecrement: () => { dispatch(Actions.decrement(ownProps.caption)); }}}Copy the code

MapStateToProps and mapDispatchToProps are passed as parameters to the Connect of React-Redux

export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Copy the code

The connect function runs as a function, and then calls the display component Counter as an argument, just like the container component nested the display component.

Reference books

React and Redux by Mo Cheng