Redux is the most commonly used tool for state management of large React applications. Its concepts, theories and practices are worth learning, analyzing and understanding in practice, which is very helpful for the development of front-end developers’ abilities. This project will combine the difference between Redux container components and display components, and the most common connection library of Redux and React applications, react-Redux source code analysis, in order to achieve a deeper understanding of Redux and React applications.

Welcome to my personal blog

preface

The React-Redux library provides the Provider component to inject a store into an application using context. Then, you can use the connect high-order method to obtain and listen on the store, calculate the new props according to the store state and the component’s own props, and inject the component. In addition, you can compare the calculated new props to determine whether the component needs to be updated by listening on the Store.

React and Redux application structures

Provider

First, the React-Redux library provides a Provider component that injects stores into an entry component of the entire React application, usually the top-level component of the application. The Provider component uses context to pass down the store:

// The internal component gets the redux store key

const storeKey = 'store'

// Internal components

const subscriptionKey = subKey || `${storeKey}Subscription`

class Provider extends Component {

// Declare context, inject store and optional publish subscribe objects

  getChildContext() {

    return { [storeKey]: this[storeKey], [subscriptionKey]: null }

  }



  constructor(props, context) {

    super(props, context)

/ / the cache store

    this[storeKey] = props.store;

  }



  render() {

// Render the output

    return Children.only(this.props.children)

  }

}Copy the code

Example

import { Provider } from 'react-redux'

import { createStore } from 'redux'

import App from './components/App'

import reducers from './reducers'



/ / create a store

const store = createStore(todoApp, reducers)



// Pass the store as props to the Provider component;

// Provider passes store down using context

// The App component is the top-level component of our App

render(

  <Provider store={store}>

    <App/>

  </Provider>, document.getElementById('app-node')

)Copy the code

The connect method

Previously we used the Provider component to inject the Redux Store into the application. Now we need to connect the component to the Store. We also know that Redux does not provide a direct way to manipulate store state, we can only access data through its getState, or dispatch an action to change store state.

This is exactly what the React-Redux provides with the connect higher-order method.

Example

container/TodoList.js

First we create a list container component, which is responsible for retrieving the ToDO list within the component. Then we pass the ToDOS to the TodoList presentation component, along with the event callback function. When the presentation component triggers an event such as a click, the corresponding callback is called. These callbacks update the Redux store state through dispatch actions, and the react-Redux connect method is used to connect the store to the demo component, which receives

import {connect} from 'react-redux'

import TodoList from 'components/TodoList.jsx'



class TodoListContainer extends React.Component {

  constructor(props) {

    super(props)

    this.state = {todos: null, filter: null}

  }

  handleUpdateClick (todo) {

    this.props.update(todo);  

  }

  componentDidMount() {

    const { todos, filter, actions } = this.props

    if (todos.length === 0) {

      this.props.fetchTodoList(filter);

    }

  render () {

    const { todos, filter } = this.props



    return (

      <TodoList 

        todos={todos}

        filter={filter}

        handleUpdateClick={this.handleUpdateClick}

        /* others */

      />

    )

  }

}



const mapStateToProps = state => {

  return {

    todos : state.todos,

    filter: state.filter

  }

}



const mapDispatchToProps = dispatch => {

  return {

    update : (todo) => dispatch({

      type : 'UPDATE_TODO',

      payload: todo

    }),

    fetchTodoList: (filters) => dispatch({

      type : 'FETCH_TODOS',

      payload: filters

    })

  }

}



export default connect(

  mapStateToProps,

  mapDispatchToProps

)(TodoListContainer)Copy the code

components/TodoList.js

import React from 'react'

import PropTypes from 'prop-types'

import Todo from './Todo'



const TodoList = ({ todos, handleUpdateClick }) => (

  <ul>

    {todos.map(todo => (

<Todo key={todo.id} {... todo} handleUpdateClick={handleUpdateClick} />

    ))}

  </ul>

)



TodoList.propTypes = {

  todos: PropTypes.array.isRequired

  ).isRequired,

  handleUpdateClick: PropTypes.func.isRequired

}



export default TodoListCopy the code

components/Todo.js

import React from 'react'

import PropTypes from 'prop-types'



class Todo extends React.Component { 

constructor(... args) {

super(.. args);

    this.state = {

      editable: false,

      todo: this.props.todo

    }

  }

  handleClick (e) {

    this.setState({

editable: ! this.state.editable

    })

  }

  update () {

    this.props.handleUpdateClick({

. this.state.todo

      text: this.refs.content.innerText

    })

  }

  render () {

    return (

      <li

        onClick={this.handleClick}

        style={{

          contentEditable: editable ? 'true' : 'false'

        }}

      >

        <p ref="content">{text}</p>

        <button onClick={this.update}>Save</button>

      </li>

    )

  }



Todo.propTypes = {

  handleUpdateClick: PropTypes.func.isRequired,

  text: PropTypes.string.isRequired

}



export default TodoCopy the code

Container components versus presentation components

When Redux is used as the state management Container for React applications, Components are often divided into Container Components and Presentational Components.

Presentational Components Container Components
The target UI presentation (HTML structure and style) Business logic (get data, update status)
Perception Redux There is no There are
The data source props Subscription Redux store
Change data Call the callback function passed by props Dispatch Redux actions
reusable Independence is strong The service coupling is high

Most of the code in your application is writing presentation components and then using container components to connect those presentation components to the Redux Store.

Connect () source code analysis

React-redux source logic

connectHOC = connectAdvanced;

MergePropsFactories = defaultMergePropsFactories;

selectorFactory = defaultSelectorFactory;

function connect (

  mapStateToProps,

  mapDispatchToProps,

  mergeProps,

  {

  pure = true,

AreStatesEqual = strictEqual, // Compare strictEqual

AreOwnPropsEqual = shallowEqual, // Shallow comparison

  areStatePropsEqual = shallowEqual,

  areMergedPropsEqual = shallowEqual,

RenderCountProp, // The props key passed to the internal component, indicating the number of render method calls

// props/context gets the key for store

  storeKey = 'store',

. extraOptions

  } = {}

) {

  const initMapStateToProps = match(mapStateToProps, mapStateToPropsFactories, 'mapStateToProps')

  const initMapDispatchToProps = match(mapDispatchToProps, mapDispatchToPropsFactories, 'mapDispatchToProps')

  const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')



// Call the connectHOC method

  connectHOC(selectorFactory, {

// If mapStateToProps is false, store state is not listened on

    shouldHandleStateChanges: Boolean(mapStateToProps),

// Pass to selectorFactory

    initMapStateToProps,

    initMapDispatchToProps,

    initMergeProps,

    pure,

    areStatesEqual,

    areOwnPropsEqual,

    areStatePropsEqual,

    areMergedPropsEqual,

RenderCountProp, // The props key passed to the internal component, indicating the number of render method calls

// props/context gets the key for store

    storeKey = 'store',

. ExtraOptions // Other configuration items

  });

}Copy the code

strictEquall

function strictEqual(a, b) { return a === b }Copy the code

shallowEquall

The source code

const hasOwn = Object.prototype.hasOwnProperty

function is(x, y) {
  if (x === y) {
    returnx ! = =0|| y ! = =0 || 1 / x === 1 / y
  } else {
    returnx ! == x && y ! == y } }export default function shallowEqual(objA, objB) {
  if (is(objA, objB)) return true

  if (typeofobjA ! = ='object' || objA === null ||
      typeofobjB ! = ='object' || objB === null) {
    return false
  }

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  if(keysA.length ! == keysB.length)return false

  for (let i = 0; i < keysA.length; i++) {
    if(! hasOwn.call(objB, keysA[i]) || ! is(objA[keysA[i]], objB[keysA[i]])) {return false}}return true
}Copy the code
shallowEqual({x: {}}, {x: {}})// false
shallowEqual({x:1}, {x:1}) // trueCopy the code

ConnectAdvanced A higher-order function

The source code

function connectAdvanced (

  selectorFactory,

  {

RenderCountProp = undefined, // Props key passed to internal components, indicating the number of render method calls

// props/context gets the key for store

    storeKey = 'store',

. connectOptions

  } = {}

) {

// Get the publish subscriber key

  const subscriptionKey = storeKey + 'Subscription';

  const contextTypes = {

    [storeKey]: storeShape,

    [subscriptionKey]: subscriptionShape,

  };

  const childContextTypes = {

    [subscriptionKey]: subscriptionShape,

  };



  return function wrapWithConnect (WrappedComponent) {

    const selectorFactoryOptions = {

// If mapStateToProps is false, store state is not listened on

      shouldHandleStateChanges: Boolean(mapStateToProps),

// Pass to selectorFactory

      initMapStateToProps,

      initMapDispatchToProps,

      initMergeProps,

. connectOptions,

. others

RenderCountProp, // number of render calls

ShouldHandleStateChanges, / / whether listening store state changes

      storeKey,

      WrappedComponent

    }



// Return the Connect component with the props property extended

    return hoistStatics(Connect, WrappedComponent)

  }

}Copy the code

selectorFactory

The selectorFactory function returns a selector function, which computes the new props based on the store state, presentation props, and Dispatch, and then injects the container component.

(dispatch, options) => (state, props) => ({

  thing: state.things[props.thingId],

  saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),

})Copy the code

Note: State in redux usually refers to the state of the Redux store, not the state of the component, and the props here is the props of the passed component wrapperComponent.

The source code

function defaultSelectorFactory (dispatch, {

  initMapStateToProps,

  initMapDispatchToProps,

  initMergeProps,

. options

{})

  const mapStateToProps = initMapStateToProps(dispatch, options)

  const mapDispatchToProps = initMapDispatchToProps(dispatch, options)

  const mergeProps = initMergeProps(dispatch, options)



// If pure is true, the selector returned by selectorFactory will cache the result;

// Otherwise it always returns a new object

  const selectorFactory = options.pure

    ? pureFinalPropsSelectorFactory

    : impureFinalPropsSelectorFactory



// Finally execute the selector factory function and return a selector

  return selectorFactory(

    mapStateToProps,

    mapDispatchToProps,

    mergeProps,

    dispatch,

    options

  );

}Copy the code

pureFinalPropsSelectorFactory

function pureFinalPropsSelectorFactory (

  mapStateToProps,

  mapDispatchToProps,

  mergeProps,

  dispatch,

  { areStatesEqual, areOwnPropsEqual, areStatePropsEqual }

) {

  let hasRunAtLeastOnce = false

  let state

  let ownProps

  let stateProps

  let dispatchProps

  let mergedProps



// Return props or state after the merge

// handleSubsequentCalls are merged after the change; HandleFirstCall is first called

  return function pureFinalPropsSelector(nextState, nextOwnProps) {

    return hasRunAtLeastOnce

      ? handleSubsequentCalls(nextState, nextOwnProps)

    : handleFirstCall(nextState, nextOwnProps)

  }  

}Copy the code

handleFirstCall

function handleFirstCall(firstState, firstOwnProps) {

  state = firstState

  ownProps = firstOwnProps

StateProps = mapStateToProps(state, ownProps) // Store State Specifies the props mapped to the component

  dispatchProps = mapDispatchToProps(dispatch, ownProps)

MergedProps = mergeProps(stateProps, dispatchProps, ownProps) // Combined

  hasRunAtLeastOnce = true

  return mergedProps

}Copy the code

defaultMergeProps

export function defaultMergeProps(stateProps, dispatchProps, ownProps) {

// The props function is merged by default

return { ... ownProps, ... stateProps, ... dispatchProps }

}Copy the code

handleSubsequentCalls

function handleSubsequentCalls(nextState, nextOwnProps) {

// shallowEqual shallow comparison

const propsChanged = ! areOwnPropsEqual(nextOwnProps, ownProps)

/ / deep comparison

const stateChanged = ! areStatesEqual(nextState, state)

  state = nextState

  ownProps = nextOwnProps



// Handle the merge after the props or state change

// Store state and component props changed

  if (propsChanged && stateChanged) return handleNewPropsAndNewState()

  if (propsChanged) return handleNewProps()

  if (stateChanged) return handleNewState()



  return mergedProps

}Copy the code

The calculation returns the new props

Whenever the presentable component changes its props, it needs to return the new merged props and update the container component, regardless of whether the store state has changed:

// Only demonstrative component props were changed

function handleNewProps() {

// mapStateToProps computes whether to rely on presentable component props

  if (mapStateToProps.dependsOnOwnProps)

    stateProps = mapStateToProps(state, ownProps)

// mapDispatchToProps calculates whether to rely on presentable component props

  if (mapDispatchToProps.dependsOnOwnProps)

    dispatchProps = mapDispatchToProps(dispatch, ownProps)



  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)



  return mergedProps

}

// The props and store state of the presentation component were changed

function handleNewPropsAndNewState() {

  stateProps = mapStateToProps(state, ownProps)

// mapDispatchToProps calculates whether to rely on presentable component props

  if (mapDispatchToProps.dependsOnOwnProps)

    dispatchProps = mapDispatchToProps(dispatch, ownProps)



  mergedProps = mergeProps(stateProps, dispatchProps, ownProps)



  return mergedProps

}Copy the code

Calculation returns stateProps

In general, changes to container components are driven by store State changes, so only store State changes are common, and this is where Immutable changes are important: do not use the toJS() method within the mapStateToProps method.

If the props object returned by mapStateToProps was not changed, the method does not need to recalcitate, and the combined props object is returned. If the value returned by the selector trace is changed, false is returned. Container components do not trigger changes.

Because shallow comparisons are used when comparing the result returned by mapStateToProps multiple times, the Immutable.tojs () method is not recommended. It returns a new object each time, and comparison returns false, while Immutable, which does not change, returns true. Can reduce unnecessary re-rendering.

// Only store state changes

function handleNewState() {

  const nextStateProps = mapStateToProps(state, ownProps)

/ / light

const statePropsChanged = ! areStatePropsEqual(nextStateProps, stateProps)

  stateProps = nextStateProps



// If the calculated new props are changed, the new combined props need to be recalculated

  if (statePropsChanged) {

    mergedProps = mergeProps(stateProps, dispatchProps, ownProps)

  }



// If the new stateProps is not changed, then return the combined props calculated last time;

// Then the selector trace object will return false when it compares the return value twice for any change;

// Otherwise return the props object newly merged using the mergeProps() method, and the change comparison will return true

  return mergedProps

}Copy the code

hoist-non-react-statics

Similar to Object.assign, copy non-React static properties or methods of the child component to the parent component. React-related properties or methods are not overwritten but merged.

hoistStatics(Connect, WrappedComponent)Copy the code

Connect Component

The react-Redux component uses the Provider component to inject the store into the context. The react-Redux component uses the Provider component to insert the store into the context. The Connect component then receives a store through the context and adds a subscription to the store:

class Connect extends Component {

  constructor(props, context) {

    super(props, context)



    this.state = {}

This. renderCount = 0 // render calls start with 0

// Get the store, props, or context method

    this.store = props[storeKey] || context[storeKey]

// Whether to pass the store as props

    this.propsMode = Boolean(props[storeKey])



// Initializes selector

    this.initSelector()

// Initialize the store subscription

    this.initSubscription()

  }



  componentDidMount() {

// No need to listen for state changes

if (! shouldHandleStateChanges) return

// The publish subscriber performs subscriptions

    this.subscription.trySubscribe()

/ / the selector

    this.selector.run(this.props)

// Force the update if it is needed

    if (this.selector.shouldComponentUpdate) this.forceUpdate()

  }



// Render component elements

  render() {

    const selector = this.selector

selector.shouldComponentUpdate = false; // Reset whether to update to the default false



// merge the props from the redux Store State transformation mapping into the passed component

    return createElement(WrappedComponent, this.addExtraProps(selector.props))

  }

}Copy the code

addExtraProps()

Add additional props attributes to props:

// Add additional props

addExtraProps(props) {

const withExtras = { ... props }

if (renderCountProp) withExtras[renderCountProp] = this.renderCount++; // render number of calls

  if (this.propsMode && this.subscription) withExtras[subscriptionKey] = this.subscription



  return withExtras

}Copy the code

Initialize Selector trace object initSelector

According to the store state of the redux and the props of the component, the Selector calculates the new props to be injected into the component and caches the new props. Then, when executing the Selector again, it compares the obtained props to determine whether the component needs to be updated. If the props changes, the component needs to be updated. Otherwise, it will not be updated.

Initialize selector to trace the selector object and its associated state and data using the initSelector method:

// Initializes selector

initSelector() {

// Create a selector using the selector factory function

  const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)

// Connect the selector and redux store state of the component

  this.selector = makeSelectorStateful(sourceSelector, this.store)

// Performs the component's selector function

  this.selector.run(this.props)

}Copy the code

MakeSelectorStateful ()

Create a selector trace object to track the result of the selector function:

function makeSelectorStateful(sourceSelector, store) {

// Returns the selector trace object, which tracks the result returned by the selector passed in

  const selector = {

// Performs the component's selector function

    run: function runComponentSelector(props) {

// Execute the selector function passed in according to the store state and component props, and calculate the nextProps

      const nextProps = sourceSelector(store.getState(), props)

// Compare the nextProps and the cache props;

// false, then update the cached props and mark selector to be updated

if (nextProps ! == selector.props || selector.error) {

The selector. ShouldComponentUpdate = true / / tag needs to be updated

Selector. Props = nextProps // cache props

        selector.error = null

      }  

    }

  }



// Return selector tracing object

  return selector

}Copy the code

Initialize subscription initSubscription

Initialize listening/subscribing to redux Store state:

// Initialize the subscription

initSubscription() {

if (! shouldHandleStateChanges) return; // No need to listen on store state



// Determine how to pass the subscribed content: props or context

  const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]

// Subscribe to object instantiation and pass in the event callback function

  this.subscription = new Subscription(this.store, 

                                       parentSub,

                                       this.onStateChange.bind(this))

// The scope of the cache subscriber publish method execution

  this.notifyNestedSubs = this.subscription.notifyNestedSubs

    .bind(this.subscription)

}Copy the code

Subscription class implementation

Component subscription store uses the subscription publisher implementation:

export default class Subscription {

  constructor(store, parentSub, onStateChange) {

    // redux store

    this.store = store

// Subscribe content

    this.parentSub = parentSub

// The callback function after the subscription content changes

    this.onStateChange = onStateChange

    this.unsubscribe = null

// An array of subscription records

    this.listeners = nullListeners

  }



/ / subscribe

  trySubscribe() {

if (! this.unsubscribe) {

// If a publication subscriber is passed, the subscription method is used to subscribe

// Otherwise use the store subscription method

      this.unsubscribe = this.parentSub

        ? this.parentSub.addNestedSub(this.onStateChange)

        : this.store.subscribe(this.onStateChange)



// Create a subscription collection object

      // { notify: function, subscribe: function }

// Wrap a publish subscriber internally;

// Publish (execute all callbacks), subscribe (add callbacks to subscription collection)

      this.listeners = createListenerCollection()

    }

  }



/ / release

  notifyNestedSubs() {

    this.listeners.notify()

  }

}Copy the code

Subscribe to the callback function

Callbacks executed after subscribing:

onStateChange() {

// The selector executes

  this.selector.run(this.props)



if (! this.selector.shouldComponentUpdate) {

// Publish directly without updating

    this.notifyNestedSubs()

  } else {

// Set the componentDidUpdate lifecycle method if updates are required

    this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate

// Call setState simultaneously to trigger component updates

    this.setState(dummyState) // dummyState = {}

  }

}



Publish changes in the componentDidUpdate lifecycle method

notifyNestedSubsOnComponentDidUpdate() {

// Clear the componentDidUpdate lifecycle method

  this.componentDidUpdate = undefined

/ / release

  this.notifyNestedSubs()

}Copy the code

Other lifecycle methods

getChildContext () {

// If there are props passing store, then we need to hide the subscription to store from any descendant component that receives and subscribes to store from the context;

// Otherwise the parent subscriber map is passed in, giving the Connect component control over the sequential flow of publish changes

  const subscription = this.propsMode ? null : this.subscription

  return { [subscriptionKey]: subscription || this.context[subscriptionKey] }

}

// New props is received

componentWillReceiveProps(nextProps) {

  this.selector.run(nextProps)

}



// Whether the component needs to be updated

shouldComponentUpdate() {

  return this.selector.shouldComponentUpdate

}



componentWillUnmount() {

/ / reset the selector

}Copy the code

Refer to the reading

  1. React with redux
  2. Smart and Dumb Components
  3. React Redux Container Pattern-