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:
- In the component
store
The state of - Listening to the
store
When the state changes, refresh the component - 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((a)= > {
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.
[Counter code] used in this article (github.com/YvetteLau/B…) Clone the myReact-redux /counter code first.
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((a)= > {
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'; ./ / the use of the connect
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:
// Pass store.getState() to mapStateToProps mapStateToProps = 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 code
Just 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:
// Pass store.dispacth to mapDispatchToProps mapDispatchToProps = (dispatch) = > ({ 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((a)= > {
const mappedState = mapStateToProps(store.getState());
//TODO does a shallow comparison, and does not setState if the state has not changed
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:
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) allows the Provider to wrap only one child. We just return 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 {
// Proptypes. shape is the same part of the code as Provider, so we can extract it later
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;
// Store. GetState () is given to this.state
this.state = mapStateToProps(this.store.getState());
this.mappedDispatch = mapDispatchToProps(this.store.dispatch);
}
componentDidMount() {
this.unsub = this.store.subscribe((a)= > {
const mappedState = mapStateToProps(this.store.getState());
//TODO does a shallow comparison. If the state has not changed, setState is not required
this.setState(mappedState);
});
}
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';
/** * mapStateToProps default unassociated state * mapDispatchToProps Default to 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... , the default value is used
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;
// Store. GetState () is given to this.state
this.state = mapStateToProps(this.store.getState(), this.props);
if (typeof mapDispatchToProps === 'function') {
this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props);
} else {
// Pass in an actionCreator object
this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch);
}
}
componentDidMount() {
this.unsub = this.store.subscribe((a)= > {
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... , the default value is used
mapDispatchToProps = defaultMapDispatchToProps;
}
return function wrapWithConnect (WrappedComponent) {
return class Connect extends Component {
static contextTypes = storeShape;
static displayName = `Connect(${getDisplayName(WrappedComponent)}) `;
constructor(props) {
super(props);
// Store. GetState () is given to this.state
this.state = mapStateToProps(store.getState(), this.props);
if(typeof mapDispatchToProps === 'function') {
this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props);
}else{
// Pass in an actionCreator object
this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch);
}
}
componentDidMount() {
this.unsub = store.subscribe((a)= > {
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 to
<MyContext.Provider>
的value
In (context value) - Child components were
<MyContext.Provider>
The parcel
import React from 'react';
import MyContext from './Context';
import Content from './Content';
class Pannel extends React.Component {
state = {
theme: {
color: 'rgb(0, 51, 254)'
}
}
render() {
return (
// The attribute name must be value
<MyContext.Provider value={this.state.theme}>
<Content />
</MyContext.Provider>)}}Copy the code
Descendant components (content.js)
Class components
- define
Class.contextType
:static contextType = ThemeContext
; - through
this.context
To obtain<ThemeContext.Provider>
中value
The content of (i.econtext
Value)
/ / class components
import React from 'react';
import ThemeContext from './Context';
class Content extends React.Component {
// Once the contextType is defined, the contents of the themecontext. Provider value can be retrieved from this.context
static contextType = ThemeContext;
render() {
return (
<div style={{color: `2px solidThe ${this.context.color} `}} >
//....
</div>)}}Copy the code
Function component
- The child element is wrapped in
<ThemeContext.Consumer>
中 <ThemeContext.Consumer>
The child element of is a function, the input parametercontext
Value (Provider
To provide thevalue
). This is a{color: XXX}
import React from 'react';
import ThemeContext from './Context';
export default function Content() {
return (
<ThemeContext.Consumer>
{
context => (
<div style={{color: `2px solidThe ${context.color} `}} >
//....
</div>)}</ThemeContext.Consumer>)}Copy the code
The context API (old)
use
- Defines the root component
childContextTypes
(validationgetChildContext
Type returned) - define
getChildContext
methods
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)'
}
}
render() {
return (
// The attribute name must be value
<>
<Content />
</>)}}Copy the code
Descendant components (content.js)
- Defines descendant components
contextTypes
(Declare and validate the type of state you want to get) - 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 = {
theme: PropTypes.object
};
render() {
return (
<div style={{color: `2px solidThe ${this.context.theme.color} `}} >
//....
</div>)}}Copy the code
Reference links:
- React-redux source: github.com/reduxjs/rea…
- React-redux (2): connect juejin.cn/post/684490…
- Write a React-redux from scratch juejin.cn/post/684490…