The reading time is about 15~20 minutes
preface
React officially brings us the Hooks API in version 16.8. What are Hooks? In short, it’s an aid to functional components, allowing us to use state and other React features without having to write class components. According to the official website, Hooks bring many benefits, of which I am deeply impressed by these:
- Compared to functional components
class
Components can often simplify a lot of code. - Without the constraints of a life cycle, some interrelated logic does not have to be forcibly divided. For example, in
componentDidMount
The timer is set in thecomponentWillUnmount
Remove; Or incomponentDidMount
Get the initial data, but remember incomponentDidUpdate
Is updated in. This logic is due touseEffect
The introduction of hooks to be written in one place effectively avoids some common bugs. - Effective reduction with variability
this
To deal with.
If that’s what Hooks are doing, why don’t you get in the car and try? So I upgraded the React and React-DOM of the technical project to version 16.8.6 and progressively tried out Hooks in the new component as recommended by the authorities. I have to say, it feels pretty good. It does knock a lot less code, but one notable thing is the use of react-Redux.
This article is not a basic tutorial on Hooks, so readers are advised to scan sections 3 and 4 of the official documentation to get a feel for the Hooks API.
The problem
React-redux Connect: react-redux connect: react-redux connect: Redux connect: Redux connect: Redux connect: Redux connect
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";
function Form(props) {
const {
formId
formData,
queryFormData,
submitFormData,
} = props;
useEffect((a)= > {
// Request form data
queryFormData(formId);
},
// Specify dependencies to prevent repeated requests when the component is rerendered
[queryFormData, formId]
);
// Process the submission
const handleSubmit = usefieldValues= > {
submitFormData(fieldValues);
}
return (
<FormUI
data={formData}
onSubmit={handleSubmit}
/>) } function mapStateToProps(state) { return { formData: state.formData }; } function mapDispatchToProps(dispatch, ownProps) {// withRouter const {history} = ownProps; return { queryFormData(formId) { return dispatch(queryFormData(formId)); }, SubmitFormData (fieldValues) {return dispatch(submitFormData(fieldValues)). Then (res) => {// Commit successfully and redirect to home page history.push('/home'); }; } } } export default withRouter(connect(mapStateToProps, mapDispatchToProps)(React.memo(Form));Copy the code
The above code describes a simple form component, queryFormData prop request form data generated by mapDispatchToProps, and useEffect to honestly record dependencies, preventing re-render components from repeating requests to the background; Submit the form data using submitFormData Prop generated by mapDispatchToProps, and navigate back to the home page using the history Prop program provided by React-Router after submitting the form data successfully. Get the data back from the background using the formData Prop generated by mapStateToProps. What does it look like?
There’s something wrong with it.
The problem is that the second parameter to mapDispatchToProps — ownProps:
function mapDispatchToProps(dispatch, ownProps) { // ** The problem is this ownProps!! **
const{ history } = ownProps; . }Copy the code
In the example above we need to use the history prop passed in by the withRouter of the React-Router for programmatic navigation, so we use the second parameter ownProps of mapDispatchToProps. The react-Redux website, however, may not be as compelling:
If we declare mapDispatchToProps with the second parameter (even if the ownProps was not actually used after the declaration), then mapDispatchTopProps will be called every time the Connected component receives a new prop. What does that mean? Since mapDispatchToProps will return a brand new object when called (queryFormData and submitFormData above will also be brand new functions), So this will cause the queryFormData and submitFormData prop passed above to
React-redux: react-redux:
// selectorFactory.js.// This function is called when the Connected component receives new props
function handleNewProps() {
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
If the second parameter (ownProps) is used when declaring mapDispatchToProps, this will be marked true
if (mapDispatchToProps.dependsOnOwnProps)
// re-invoke mapDispatchToProps to update dispatchProps
dispatchProps = mapDispatchToProps(dispatch, ownProps)
// mergeProps = {... ownProps, ... stateProps, ... dispatchProps }
// Finally pass in the connect wrapped component
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
...
Copy the code
The solution
1. The most convenient option
Pass an empty array to the second argument to useEffect:
function Form(props) {
const {
formId,
queryFormData,
...
} = props;
useEffect((a)= > {
// Request form data
queryFormData(formId);
},
// Pass in an empty array like componentDidMount[]); . }Copy the code
This approach tells useEffect that the method to be called inside doesn’t have any external dependencies — in other words, it doesn’t need to be repeated (during dependency updates), so useEffect will only call the method passed in after the first rendering of the component, similar to componentDidMount. However, this approach, while feasible, is a trick to React (we rely on queryFormData and formId from props), and it’s easy to get stuck (see the React Hooks FAQ). In fact, if we had installed eslint-plugin-react-hooks on our project, as the React official advice would have done, we would have received an ESLint alert. So for the sake of code quality, try not to be lazy with this approach.
2. Do not useownProps
parameter
Put the logic needed to use ownProps inside the component:
function Form(props) {
const {
formId
queryFormData,
submitFormData,
history
...
} = props;
useEffect((a)= > {
queryFormData(formId);
},
QueryFormData is stable because ownProps was not used when mapDispatchToProps was declared
[queryFormData, formId]
);
const handleSubmit = fieldValues= > {
submitFormData(fieldValues)
// Migrate the logic needed for ownProps to the component definition (with redux-thunk middleware, return Promise)
.then(res= > {
history.push('/home'); }); }... }... function mapDispatchToProps(dispatch) {// No longer declare the ownProps parameter
return {
queryFormData(formId) {
return dispatch(queryFormData(formId));
},
submitFormData(fieldValues) {
returndispatch(submitFormData(fieldValues)); }}}...Copy the code
This is also a less modified approach, but has the disadvantage of forcing the associated logic into two places (mapDispatchToProps and components). We also need to add a comment to remind maintainers not to use the ownProps parameter in mapDispatchToProps (in fact, mapStateToProps has similar concerns if you look at the source code above), which is not very reliable.
3. Do not usemapDispatchToProps
If mapDispatchToProps is not passed to connect, the wrapped component will receive the Dispatch prop, allowing the logic to use dispatch to be written inside the component:
.// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";
function Form(props) {
const {
formId
history,
dispatch
...
} = props;
useEffect((a)= > {
// Use dispatches within components
// Note that queryFormData comes from import, not props, and doesn't change, so you don't need to write it into the dependency array
dispatch(queryFormData(formId))
},
[dispatch, formId]
);
const handleSubmit = fieldValues= > {
// Use dispatches within components
dispatch(submitFormData(fieldValues))
.then(res= > {
history.push('/home'); }); }... }...// mapDispatchToProps is not passed
export default withRouter(connect(mapStateToProps, null)(React.memo(Form));
Copy the code
This is personally recommended as it eliminates the need to split the associated logic (which is where hooks were originally intended), and putting dispatch logic in useEffect also allows eslint to automatically check dependencies and avoid bugs. The other problem, of course, is that if you need to request many cgis, it would seem bloated to put all the relevant logic in useEffect? At this point we can use useCallback:
import { actionCreator1 } from "@/data/actionCreator1/action";
import { actionCreator2 } from "@/data/actionCreator2/action";
import { actionCreator3 } from "@/data/actionCreator3/action"; . function Form(props) {const {
dep1,
dep2,
dep3,
dispatch
...
} = props;
// Use useCallback to pull out the useEffect function
const fetchUrl1() = useCallback((a)= > {
dispatch(actionCreator1(dep1));
.then(res= >{... }) .catch(err= >{... }); }, [dispatch, dep1]);The second argument to useCallback, like useEffect, is a dependency
const fetchUrl2() = useCallback((a)= > {
dispatch(actionCreator2(dep2));
.then(res= >{... }) .catch(err= >{... }); }, [dispatch, dep2]);const fetchUrl3() = useCallback((a)= > {
dispatch(actionCreator3(dep3));
.then(res= >{... }) .catch(err= >{... }); }, [dispatch, dep3]); useEffect((a)= > {
fetchUrl1();
fetchUrl2();
fetchUrl3();
},
// useEffect's direct dependency becomes the useCallback wrapped function
[fetchUrl1, fetchUrl2, fetchUrl3]
);
// handleSubmit should also be wrapped with useCallback to avoid unnecessary re-render of child components
const handleSubmit = useCallback(fieldValues= > {
// Use dispatches within components
dispatch(submitFormData(fieldValues))
.then(res= > {
history.push('/home');
});
});
return (
<FormUI
data={formData}
onSubmit={handleSubmit}
/>)}...Copy the code
UseCallback returns a Memorized version of the function it wraps, and memorized functions are not updated as long as the dependencies remain unchanged. Using this feature, we can use useCallback to encapsulate the logic to be invoked in useEffect. Then we just need to add memorized functions to useEffect dependencies and it will work normally.
However, as mentioned earlier, the ownProps parameter in mapStateToProps also causes the mapStateToProps to be re-called, generating new state props:
// This function is called when the Connected component receives new props
function handleNewProps() {
// If the ownProps parameter is used when declaring mapStateToProps, a new stateProps is generated!
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
Copy the code
Therefore, useEffect can still fail dependency checks if it relies on these state props (for example, if the state props is a reference type).
Use react-Redux hooks APIs (recommended)
Since the previous solutions are more or less spoof, try the official hooks APIs that React Redux brought with it in v7.1.0. Here’s how to use them.
Main APIS used:
import { useSelector, useDispatch } from 'react-redux'
// The selector function is used similarly to mapStateToProps in that it will return the value of useSelector, but unlike mapStateToProps, it can return any type of value (not just an object), There is no second argument, ownProps (because it can be accessed inside the component via closures)
const result : any = useSelector(selector : Function, equalityFn? : Function)
const dispatch = useDispatch()
Copy the code
Use:
. import { useSelector, useDispatch }from "react-redux";
// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";
function Form(props) {
const {
formId
history,
dispatch
...
} = props;
const dispatch = useDispatch();
useEffect((a)= > {
dispatch(queryFormData(formId))
},
[dispatch, formId]
);
const handleSubmit = useCallback(fieldValues= > {
dispatch(submitFormData(fieldValues))
.then(res= > {
history.push('/home');
});
}, [dispatch, history]);
const formData = useSelector(state= >state.formData;) ; . return (<FormUI
data={formData}
onSubmit={handleSubmit}
/>); }... // Do not use connect export default withRouter(react.Memo (Form));Copy the code
As you can see, this is very similar to the “Don’t use mapDispatchToProps” approach, which is to pass in the Dispatch and define the logic that needs to use the Dispatch inside the component. The biggest difference is to change the place where state is extracted from from mapStateToProps to useSelector. They are used similarly, but with special care if you want the latter to return an object like the former:
Since useSelector’s internal default is to use === to determine if two previous selectors evaluate the same (which triggers the component re-render), if the selector returns an object, That actually generates a new object every time useSelector is called when it executes, which makes the component meaningless re-render. To solve this problem, you can use libraries like Reselect to create selectors with memoized effects, or pass the react-Redux built-in shallowEqual to useSelector’s second argument (the comparison function) :
import { useSelector, shallowEqual } from 'react-redux'
const selector = state= > ({
a: state.a,
b: state.b
});
const data = useSelector(selector, shallowEqual);
Copy the code
Use Hooks instead of Redux?
Since Hooks first appeared, one of the hottest topics in the community has been using Hooks to implement global state management. A common approach is as follows:
-
Related Hooks: useContext, useReducer
-
Implementation:
import { createContext, useContext, useReducer, memo } from 'react'; function reducer(state, action) { switch (action.type) { case 'UPDATE_HEADER_COLOR': return { ...state, headerColor: 'yellow' }; case 'UPDATE_CONTENT_COLOR': return { ...state, contentColor: 'green' }; default: break; }}// Create a context const Store = createContext(null); // as global state const initState = { headerColor: 'red'.contentColor: 'blue' }; const App = (a)= > { const [state, dispatch] = useReducer(reducer, initState); // Inject global state and dispatch methods at the root return( <Store.Provider value={{ state, dispatch }}> <Header/> <Content/> </Store.Provider> ); }; Const Header = memo(() => {// Get the global state and dispatch const {state, dispatch} = useContext(Store); return ( <header style={{backgroundColor: state.headerColor}} onClick={() => dispatch('UPDATE_HEADER_COLOR')} /> ); }); const Content = memo(() => { const { state, dispatch } = useContext(Store); return ( <div style={{backgroundColor: state.contentColor}} onClick={() => dispatch('UPDATE_CONTENT_COLOR')} /> ); });Copy the code
The above code uses the context to inject a global state and dispatch actions function into all the child elements wrapped by the Provider. Combined with the useReducer, it looks like a poor man’s version of Redux.
However, the code above has performance implications: Whether we click on
or
<Header/>
and<Content/>
Will be re-rendered!
Since it is clear that they both consume the same state (though only part of it), when the global state is updated, all consumers will be updated as well.
But haven’t we already wrapped the components in memo?
Yes, the Memo holds updates from props for us, but state is injected inside the component via the useContext hook, bypassing the uppermost memo.
So is there a way to avoid forced updates? Dan Abramov points out a few ways to do this:
-
Splitting the Context (recommended). By splitting the global State into different contexts as needed, there will naturally be no interaction resulting in unnecessary rerendering;
-
Divide the component into two parts and wrap the inner layer with Memo:
const Header = (a)= > { const { state, dispatch } = useContext(Store); return memo(<ThemedHeader theme={state.headerColor} dispatch={dispatch} />); }; const ThemedHeader = memo(({theme, dispatch}) => { return ( <header style={{backgroundColor: theme}} onClick={() => dispatch('UPDATE_HEADER_COLOR')} /> ); }); Copy the code
-
Use useMemo hook. The idea is the same as above, but without breaking it into two components:
const Header = (a)= > { const { state, dispatch } = useContext(Store); return useMemo( (a)= > ( <header style={{backgroundColor: state.headerColor}} onClick={()= > dispatch('UPDATE_HEADER_COLOR')} /> ), [state.headerColor, dispatch] ); }; Copy the code
React-redux. React-redux, react-redux, react-redux, react-redux, react-redux, react-redux, react-redux, react-redux, react-redux, react-redux In addition, we will also face the following problems:
- You need to implement combineReducers and other auxiliary functions by yourself (if needed)
- Lost middleware support for the Redux ecosystem
- Lost debugging tools such as Redux DevTools
- Out of the hole is not good for asking for help…
Therefore, unless you are working on a personal or technical project where the need for state management is simple, or you simply want to build wheels to practice, you are not advised to abandon a mature state management solution such as Redux because it is not cost-effective.
conclusion
React-redux Hooks provide developers with a clean experience using the React-Redux connect APIs, which allow them to use the ownProps parameters in conjunction with the react-Redux connect APIs. Use the officially provided Hooks API.
In addition, the use of Hooks self-built global state management way is feasible in the small project, however, want to use in the larger, formal business, at least spend idea to solve the performance problems, and the problem is the React – Redux tools already spend a lot of time to help us solve, there doesn’t seem to be any good reason to abandon them.
reference
- React-redux official document
- React
- Preventing rerenders with React.memo and useContext hook
Recommended reading
- The History and Implementation of React-Redux
- UseEffect complete guide
Pay attention to [IVWEB community] public number to get the latest weekly articles, leading to the top of life!