(Add star to front end to improve front end skills)
Author: Front-end universe public number/Liu Xiaoxi
What is the react – redux
React-redux is the official Redux binding library. It helps us connect the UI layer to the data layer. The purpose of this article is not to introduce you to the use of React-Redux, but to implement a simple react-Redux that will hopefully help you.
Let’s first consider how our React project would work with Redux if we didn’t use React-Redux.
For each component that needs to be used with Redux, we need to do the following:
-
Gets the state in store in the component
-
Listen for changes in state in the Store and refresh the component when the state changes
-
Remove listening for state changes when a component is uninstalled.
As follows:
import React from 'react'; import store from '.. /store'; import actions from '.. /store/actions/counter'; Reducer is combineReducer({counter,... * the structure of the state for * {}) * counter: {number: 0}, *... * } */class Counter extends React.Component { constructor(props) { super(props); this.state = { number: store.getState().counter.number } } componentDidMount() { this.unsub = store.subscribe(() => { if(this.state.number === store.getState().counter.number) { return; } this.setState({ number: store.getState().counter.number }); }); } render() { return ( <div> <p>{`number: ${this.state.number}`}</p> <button onClick={() => {store.dispatch(actions.add(2))}}>+</button> <button onClick={() => {store.dispatch(actions.minus(2))}}>-</button> <div> ) } componentWillUnmount() { this.unsub(); }}
Copy the code
If there are many components in our project that need to be used with Redux, they all need to write this logic repeatedly. Obviously, we need to find a way to reuse this part of the logic, or we’ll look stupid. We know that high-order components in React allow reuse of logic.
In this paper, we used the Counter code in https://github.com/YvetteLau/Blog myreact redux/Counter, suggested clone code first, of course, if think this good, give a star to encourage.
Logic reuse
Create a react-redux folder in the SRC directory. Create subsequent files in this folder.
Create the connect.js file
File created under react-redux/components:
We’ll write the repetitive logic in Connect.
import React, { Component } from 'react'; import store from '.. /.. /store'; export default function connect (WrappedComponent) { return class Connect extends Component { constructor(props) { super(props); this.state = store.getState(); } componentDidMount() { this.unsub = store.subscribe(() => { this.setState({ this.setState(store.getState()); }); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {... this.state} {... this.props}/> ) } }}
Copy the code
One small problem is that although the logic is repetitive, each component needs different data and should not pass all state to the component, so we want connect to be able to tell connect what the required state content is when we call connect. In addition, the component may need to change its state, so you also need to tell CONNECT which actions it needs to dispatch, otherwise Connect won’t know which actions to bind to you.
To do this, we added two new parameters: mapStateToProps and mapDispatchToProps, which are responsible for telling the CONNECT component what state is required and what action is to be delivered.
MapStateToProps and mapDispatchToProps
We know what mapStateToProps and mapDispatchToProps are for, but so far we don’t know what format these parameters should be passed to Connect to use.
import { connect } from 'react-redux'; . Export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Copy the code
-
MapStateToProps tells CONNECT what state the component needs to bind to.
The mapStateToProps needs to pick the state the component needs from the entire state, but when we call connect, we don’t get store, but we do get store from inside connect. So, We define mapStateToProps as a function, call it inside connect, pass it state from store, and then pass the result returned by the function as a property to the component. Component from this.props.XXX. Therefore, the format for mapStateToProps should look something like this:
/ / will store. GetState () passed to mapStateToPropsmapStateToProps = state = > ({number: state. Counter. Number});
Copy the code -
MapDispatchToProps tells CONNECT which actions the component needs to bind to.
Recall that the component dispatches actions: store.dispatch({actions.add(2)}). After the connect wrap, we still need to be able to deliver the action, which must be something like this.props.XXX().
Call this.props. Add (2) to dispatch store.dispatch({actions. Add (2)}). (num) => {store.dispatch({actions. Add (num)})}. The properties passed to the component look like this:
{ add: (num) => { store.dispatch(actions.add(num)) }, minus: (num) => { store.dispatch(actions.minus(num)) }}
Copy the codeJust like mapStateToProps, when we call connect, we don’t get store.dispatch, so we need to design mapDispatchToProps as a function called inside connect, This will pass it store.dispatch. So, mapStateToProps should look like this:
/ / will store. Dispacth passed to mapDispatchToPropsmapDispatchToProps = (dispatch) = > ({the add: (num) => { dispatch(actions.add(num)) }, minus: (num) => { dispatch(actions.minus(num)) }})
Copy the code
Now that we know the format of mapStateToProps and mapDispatchToProps, it’s time to further improve Connect.
The connect version 1.0
import React, { Component } from 'react'; import store from '.. /.. /store'; export default function connect (mapStateToProps, mapDispatchToProps) { return function wrapWithConnect (WrappedComponent) { return class Connect extends Component { constructor(props) { super(props); this.state = mapStateToProps(store.getState()); this.mappedDispatch = mapDispatchToProps(store.dispatch); } componentDidMount() { this.unsub = store.subscribe(() => { const mappedState = mapStateToProps(store.getState()); SetState this.setState(mappedState); //TODO does a shallow comparison, if the state has not changed, do not setState this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {... this.props} {... this.state} {... this.mappedDispatch} /> ) } } }}
Copy the code
As we know, connect is provided as a method of the React-Redux library, so it is not possible to import a store directly from connect.js. The store should be passed in by an application that uses React-Redux. React passes data in one of two ways: through properties props or through the context object Context. Components wrapped in connect are distributed across the application, and context is designed to share data that is “global” to a component tree.
We need to put store on the context so that all descendants of the root component can get store. Of course we could write code for this in our own application, but obviously this code is repeated in every application. So we’ll encapsulate this inside the React-Redux as well.
Here, we’re using the old Context API (we’re using the old Context API because of the code we implemented for the React-Redux 4.x branch).
Provider
We need to provide a Provider component whose function is to take the store passed by the application and hang it on the context so that its descendants can access the Store through the context object.
Create the provider.js file
File created under react-redux/components:
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class Provider extends Component { static childContextTypes = { store: PropTypes.shape({ subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, getState: PropTypes.func.isRequired }).isRequired } constructor(props) { super(props); this.store = props.store; } getChildContext() { return { store: This.store}} render() {/** * return Children. Only (this.props. Children) This.props. Children */ return this.props. Children}}
Copy the code
Create a new index.js file
File created in react-redux directory:
This file does only one thing, which is to export connect and Provider
import connect from './components/connect'; import Provider from './components/Provider'; export { connect, Provider}
Copy the code
The use of the Provider
To use it, we simply import the Provider and pass the store to the Provider.
import React, { Component } from 'react'; import { Provider } from '.. /react-redux'; import store from './store'; import Counter from './Counter'; export default class App extends Component { render() { return ( <Provider store={store}> <Counter /> </Provider> ) }}
Copy the code
At this point, the source code and usage of the Provider are clear, but the corresponding connect needs to be modified. For versatility, we need to fetch the store from the context instead of importing it.
The connect version 2.0
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default function connect(mapStateToProps, mapDispatchToProps) { return function wrapWithConnect(WrappedComponent) { return class Connect extends Component { Static contextTypes = {store: PropTypes. Shape ({subscribe: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired, getState: PropTypes.func.isRequired }).isRequired } constructor(props, context) { super(props, context); this.store = context.store; State this.state = mapStateToProps(this.store.getState()); this.mappedDispatch = mapDispatchToProps(this.store.dispatch); } componentDidMount() { this.unsub = this.store.subscribe(() => { const mappedState = mapStateToProps(this.store.getState()); SetState this.setState(mappedState); //TODO does a shallow comparison. }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {... this.props} {... this.state} {... this.mappedDispatch} /> ) } } }}
Copy the code
Use connect to associate the data in the Counter and store.
import React, { Component } from 'react'; import { connect } from '.. /react-redux'; import actions from '.. /store/actions/counter'; class Counter extends Component { render() { return ( <div> <p>{`number: ${this.props.number}`}</p> <button onClick={() => { this.props.add(2) }}>+</button> <button onClick={() => { this.props.minus(2) }}>-</button> </div> ) }}const mapStateToProps = state => ({ number: state.counter.number}); const mapDispatchToProps = (dispatch) => ({ add: (num) => { dispatch(actions.add(num)) }, minus: (num) => { dispatch(actions.minus(num)) }}); export default connect(mapStateToProps, mapDispatchToProps)(Counter);
Copy the code
Store /actions/counter.js is defined as follows:
import { INCREMENT, DECREMENT } from '.. /action-types'; const counter = { add(number) { return { type: INCREMENT, number } }, minus(number) { return { type: DECREMENT, number } }}export default counter;
Copy the code
At this point, our React-Redux library is ready to use, but there are many details to be worked out:
-
So the definition of mapDispatchToProps is a little bit tricky to write, if you remember bindActionCreators from redux, with this method we were able to pass actionCreator to connect, The transformation is then performed within CONNECT.
-
The PropType rules for connect and stores in providers can be extracted to avoid code redundancy
-
MapStateToProps and mapDispatchToProps can provide default values. The default value of mapStateToProps is state => ({}). Unassociated state;
The default value of mapDispatchToProps is dispatch => ({dispatch}), passing the store.dispatch method as an attribute to the wrapped attribute.
-
So far, we just passed store.getState() to mapStateToProps, but it is possible that when filtering the required state, we will need to use the properties of the component itself, so we can pass the properties of the component to mapStateToProps as well. It also passes its own property to mapDispatchToProps for the same reason.
The connect version 3.0
We’ve pulled out the Store PropType rule and put it in the utils/ STO0 0.
The shallow comparison code is in the utils/ shallowequal.js file. The general shallow comparison function is not listed here.
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import storeShape from '.. /utils/storeShape'; import shallowEqual from '.. /utils/shallowEqual'; /** * The default value of mapDispatchToProps is dispatch => ({dispatch}), Pass the 'store.dispatch' method as a property to the component */const defaultMapStateToProps = state => ({}); const defaultMapDispatchToProps = dispatch => ({ dispatch }); export default function connect(mapStateToProps, mapDispatchToProps) { if(! mapStateToProps) { mapStateToProps = defaultMapStateToProps; } if (! MapDispatchToProps) {// If mapDispatchToProps is null/undefined/false... When using the default values mapDispatchToProps = defaultMapDispatchToProps; } return function wrapWithConnect(WrappedComponent) { return class Connect extends Component { static contextTypes = { store: storeShape }; constructor(props, context) { super(props, context); this.store = context.store; State this.state = mapStateToProps(this.store.getState(), this.props); if (typeof mapDispatchToProps === 'function') { this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props); } else {// Pass an actionCreator object to this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch); } } componentDidMount() { this.unsub = this.store.subscribe(() => { const mappedState = mapStateToProps(this.store.getState(), this.props); if (shallowEqual(this.state, mappedState)) { return; } this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {... this.props} {... this.state} {... this.mappedDispatch} /> ) } } }}
Copy the code
Now, our Connect allows mapDispatchToProps to be a function or actionCreators object, and if mapStateToProps and mapDispatchToProps are either default or null, Can also perform well.
One problem, however, is that connect returns all the component names as CONNECT, which is not easy to debug. So we can add displayName to it.
The connect version 4.0
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import storeShape from '.. /utils/storeShape'; import shallowEqual from '.. /utils/shallowEqual'; /** * By default, mapDispatchToProps is not associated with state * mapDispatchToProps by default, the default value is dispatch => ({dispatch}), Pass the 'store.dispatch' method as a property to the component */ const defaultMapStateToProps = state => ({}); const defaultMapDispatchToProps = dispatch => ({ dispatch }); function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }export default function connect(mapStateToProps, mapDispatchToProps) { if(! mapStateToProps) { mapStateToProps = defaultMapStateToProps; } if(! MapDispatchToProps) {// If mapDispatchToProps is null/undefined/false... When using the default values mapDispatchToProps = defaultMapDispatchToProps; } return function wrapWithConnect (WrappedComponent) { return class Connect extends Component { static contextTypes = storeShape; static displayName = `Connect(${getDisplayName(WrappedComponent)})`; constructor(props) { super(props); State this.state = mapStateToProps(store.getState(), this.props); if(typeof mapDispatchToProps === 'function') { this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props); }else{// Pass an actionCreator object to this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch); } } componentDidMount() { this.unsub = store.subscribe(() => { const mappedState = mapStateToProps(store.getState(), this.props); if(shallowEqual(this.state, mappedState)) { return; } this.setState(mappedState); }); } componentWillUnmount() { this.unsub(); } render() { return ( <WrappedComponent {... this.props} {... this.state} {... this.mappedDispatch} /> ) } } }}
Copy the code
React-redux is basically implemented by now, but the code is not perfect. For example, the ref is missing, and this. State and this.mappedDispatch are recalcuated when the props of the component changes, without further performance optimization. You can build on that.
The react-Redux trunk branch has been rewritten using hooks, a new version of the code will be published later if time is available.
Finally, using our own writing the react – the story and the story write Todo demo, function is normal, the code in the https://github.com/YvetteLau/Blog myreact redux/Todo.
Attached is how to use the old and new context APIS:
context
There are currently two versions of the Context API. The older API will be supported in all 16.x releases, but will be removed in future releases.
The context API (new)
const MyContext = React.createContext(defaultValue);
Copy the code
Create a Context object. When React renders a component subscribed to the Context object, the component reads the current Context value from the closest matching Provider in the component tree.
Note: The defaultValue parameter takes effect only if the component’s tree does not match the Provider.
use
Context.js
First we create the Context object
import React
from
'react';
const MyContext = React.createContext(
null);
export
default MyContext;
Copy the code
Root Component (pannel.js)
-
Set the content you want to share in the value of < myContext.provider >.
-
The child component is wrapped with < myContext.provider >
import React from 'react'; import MyContext from './Context'; import Content from './Content'; class Pannel extends React. Component { state = { theme: { color: 'rgb(0, 51, Provider value={this.state. Theme}> < Content /> </ MyContext.Provider> ) } }
Copy the code
Descendant components (content.js)
Class components
-
ContextType: static contextType = ThemeContext;
-
Get the value of < themecontext.provider > via this.context.
// Class component import React from 'React '; import ThemeContext from './Context'; Class Content extends react. Component {// With contextType defined, Provider value static contextType = ThemeContext; render() { return ( < div style= {{color: ` 2px solid ${ this.context.color}`}}> //.... </ div> ) } }
Copy the code
Function component
-
The child element is wrapped in < themecontext.consumer >
-
The child element of < themecontext. Consumer> is a function that takes in the context value (the value provided by the Provider). {color: XXX}
import React
from
'react';
import ThemeContext
from
'./Context';
export
default
function
Content(
) {
return (
<
ThemeContext.Consumer>
{
context => (
<
div
style=
{{color: `
2px
solid ${
context.color}`}}>
//....
</
div>
)
}
</
ThemeContext.Consumer>
)
}
Copy the code
The context API (old)
use
-
Define the root component’s childContextTypes (validate the type returned by getChildContext)
-
Define the getChildContext method
Root Component (pannel.js)
import React from 'react'; import PropTypes from 'prop-types'; import Content from './Content'; class Pannel extends React. Component { static childContextTypes = { theme: PropTypes.object } getChildContext() { return { theme: this.state.theme } } state = { theme: { color: 'RGB (0, 51, 254)'}} to render () {return (/ / attribute names must be called value < > < the Content / > < / a >)}}
Copy the code
Descendant components (content.js)
-
Define contextTypes of descendant components (declare and validate the type of state to fetch)
-
This. Context is used to get the context content passed in.
import React
from
'react';
import PropTypes
from
'prop-types';
class
Content
extends
React.
Component {
static contextTypes = PropTypes.object;
render() {
return (
<
div
style=
{{color: `
2px
solid ${
this.context.color}`}}>
//....
</
div>
)
}
}
Copy the code
Reference links:
-
The react – story source: https://github.com/reduxjs/react-redux/tree/4.x
-
【 skilled and magical craftsmanship React – Redux (2) : connect] https://juejin.cn/post/6844903488699236359
-
【 study together made wheels (3) : starting from scratch to write a React – Redux 】 https://juejin.cn/post/6844903632702275598
Recommended reading
(Click on the title to jump to read)
React automatic state saving
Handwriting redux from scratch
How to make your JS write more beautiful
Find this article helpful? Please share it with more people
Pay attention to “front-end daqo” plus star label, improve front-end skills
Good article, I’m reading ❤️