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.
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
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
- React with redux
- Smart and Dumb Components
- React Redux Container Pattern-