做者:Alex Xu
阅读时间大约15~20minjavascript
React在16.8版本为咱们正式带来了Hooks
API。什么是Hooks
?简而言之,就是对函数式组件的一些辅助,让咱们没必要写class
形式的组件也能使用state和其余一些React特性。按照官网的介绍,Hooks
带来的好处有不少,其中让我感觉最深的主要有这几点:html
class
组件一般能够精简很多代码。componentDidMount
中设置了定时器,须要在componentWillUnmount
中清除;又或者在componentDidMount
中获取了初始数据,但要记得在componentDidUpdate
中进行更新。这些逻辑因为useEffect
hook的引入而得以写在同一个地方,能有效避免一些常见的bug。this
打交道。既然Hooks
大法这么好,不赶忙上车试试怎么行呢?因而本人把技术项目的react
和react-dom
升级到了16.8.6版本,并按官方建议,渐进式地在新组件中尝试Hooks
。不得不说,感受仍是很不错的,确实敲少了很多代码,然而有个值得注意的地方,那就是结合React-Redux
的使用。java
咱们先来看一段使用了Hooks的函数式组件结合React-Redux connect
的用法:git
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(() => {
// 请求表单数据
queryFormData(formId);
},
// 指定依赖,防止组件从新渲染时重复请求
[queryFormData, formId]
);
// 处理提交
const handleSubmit = usefieldValues => {
submitFormData(fieldValues);
}
return (
<FormUI data={formData} onSubmit={handleSubmit} /> ) } function mapStateToProps(state) { return { formData: state.formData }; } function mapDispatchToProps(dispatch, ownProps) { // withRouter传入的prop,用于编程式导航 const { history } = ownProps; return { queryFormData(formId) { return dispatch(queryFormData(formId)); }, submitFormData(fieldValues) { return dispatch(submitFormData(fieldValues)) .then(res) => { // 提交成功则重定向到主页 history.push('/home'); }; } } } export default withRouter(connect(mapStateToProps, mapDispatchToProps)(React.memo(Form)); 复制代码
上面代码描述了一个简单的表单组件,经过mapDispatchToProps
生成的queryFormData
prop请求表单数据,并在useEffect
中诚实地记录了依赖,防止组件re-render时重复请求后台;经过mapDispatchToProps
生成的submitFormData
prop提交表单数据,并在提交成功后使用React-Router提供的history
prop编程式导航回首页;经过mapStateToProps
生成的formData
prop拿到后台返回的数据。看起来彷佛妹啥毛病?github
其实有毛病。npm
问题就在于mapDispatchToProps
的第二个参数——ownProps
:编程
function mapDispatchToProps(dispatch, ownProps) { // **问题在于这个ownProps!!!**
const { history } = ownProps;
...
}
复制代码
在上面的例子中咱们须要使用React-Router的withRouter
传入的history
prop来进行编程式导航,因此使用了mapDispatchToProps
的第二个参数ownProps。然而关于这个参数,React-Redux官网上的这句话也许不是那么的引人注意:redux
若是咱们在声明mapDispatchToProps
时使用了第二个参数(即使声明后没有真的用过这个ownProps
),那么每当connected的组件接收到新的props时,mapDispatchTopProps
都会被调用。这意味着什么呢?因为mapDispatchToProps
被调用时会返回一个全新的对象(上面的queryFormData
、submitFormData
也将会是全新的函数),因此这会致使上面传入到<Form/>
中的queryFormData
和submitFormData
prop被隐式地更新,形成useEffect
的依赖检查失效,组件re-render时会重复请求后台数据。数组
对应的React-Redux源码是这段:
// selectorFactory.js
...
// 此函数在connected组件接收到new props时会被调用
function handleNewProps() {
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
// 声明mapDispatchToProps时若是使用了第二个参数(ownProps)这里会标记为true
if (mapDispatchToProps.dependsOnOwnProps)
// 从新调用mapDispatchToProps,更新dispatchProps
dispatchProps = mapDispatchToProps(dispatch, ownProps)
// mergeProps的作法实际上是:mergedProps = { ...ownProps, ...stateProps, ...dispatchProps }
// 最后传入被connect包裹的组件
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
...
复制代码
给useEffect的第二个参数传一个空数组:
function Form(props) {
const {
formId,
queryFormData,
...
} = props;
useEffect(() => {
// 请求表单数据
queryFormData(formId);
},
// 传入空数组,起到相似componentDidMount的效果
[]
);
...
}
复制代码
这种方式至关于告诉useEffect
,里面要调用的方法没有任何外部依赖——换句话说就是不须要(在依赖更新时)重复执行,因此useEffect
就只会在组件第一次渲染后调用传入的方法,起到相似componentDidMount
的效果。然而,这种方法虽然可行,但倒是一种欺骗React的行为(咱们明明依赖了来自props的queryFormData
和formId
),很容易埋坑(见React官方的Hooks FAQ)。实际上,若是咱们有遵循React官方的建议,给项目装上eslint-plugin-react-hooks
的话,这种写法就会收到eslint的告警。因此从代码质量的角度考虑,尽可能不要偷懒采用这种写法。
ownProps
参数把须要用到ownProps的逻辑放在组件内部:
function Form(props) {
const {
formId
queryFormData,
submitFormData,
history
...
} = props;
useEffect(() => {
queryFormData(formId);
},
// 因为声明mapDispatchToProps时没使用ownProps,因此queryFormData是稳定的
[queryFormData, formId]
);
const handleSubmit = fieldValues => {
submitFormData(fieldValues)
// 把须要用到ownProps的逻辑迁移到组件内定义(使用了redux-thunk中间件,返回Promise)
.then(res => {
history.push('/home');
});
}
...
}
...
function mapDispatchToProps(dispatch) { // 再也不声明ownProps参数
return {
queryFormData(formId) {
return dispatch(queryFormData(formId));
},
submitFormData(fieldValues) {
return dispatch(submitFormData(fieldValues));
}
}
}
...
复制代码
一样是改动较少的作法,但缺点是把相关联的逻辑强行分割到了两个地方(mapDispatchToProps
和组件里)。同时咱们还必须加上注释,提醒之后维护的人不要在mapDispatchToProps
里使用ownProps
参数(实际上若是有瞄过上面的源码,就会发现mapStateToProps
也有相似的顾忌),并不太靠谱。
mapDispatchToProps
若是不给connect
传入mapDispatchToProps
,那么被包裹的组件就会接收到dispatch
prop,从而能够把须要使用dispatch
的逻辑写在组件内部:
...
// action creators
import { queryFormData } from "@/data/queryFormData/action";
import { submitFormData } from "@/data/submitFormData/action";
function Form(props) {
const {
formId
history,
dispatch
...
} = props;
useEffect(() => {
// 在组件内使用dispatch
// 注意这里的queryFormData来自import,而非props,不会变,因此不用写进依赖数组
dispatch(queryFormData(formId))
},
[dispatch, formId]
);
const handleSubmit = fieldValues => {
// 在组件内使用dispatch
dispatch(submitFormData(fieldValues))
.then(res => {
history.push('/home');
});
}
...
}
...
// 不传入mapDispatchToProps
export default withRouter(connect(mapStateToProps, null)(React.memo(Form));
复制代码
这是我的比较推荐的作法,没必要分割相关联的逻辑(这也是hooks的初衷之一),同时把dispatch的相关逻辑写在useEffect里也可让eslint自动检查依赖,避免出bug。固然带来的另外一个问题是,若是须要请求不少条cgi,那把相关逻辑都写在useEffect
里好像会很臃肿?此时咱们可使用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;
// 利用useCallback把useEffect要使用的函数抽离到外部
const fetchUrl1() = useCallback(() => {
dispatch(actionCreator1(dep1));
.then(res => {...})
.catch(err => {...});
}, [dispatch, dep1]); // useCallback的第二个参数跟useEffect同样,是依赖项
const fetchUrl2() = useCallback(() => {
dispatch(actionCreator2(dep2));
.then(res => {...})
.catch(err => {...});
}, [dispatch, dep2]);
const fetchUrl3() = useCallback(() => {
dispatch(actionCreator3(dep3));
.then(res => {...})
.catch(err => {...});
}, [dispatch, dep3]);
useEffect(() => {
fetchUrl1();
fetchUrl2();
fetchUrl3();
},
// useEffect的直接依赖变成了useCallback包裹的函数
[fetchUrl1, fetchUrl2, fetchUrl3]
);
// 为了不子组件发生没必要要的re-render,handleSubmit其实也应该用useCallback包裹
const handleSubmit = useCallback(fieldValues => {
// 在组件内使用dispatch
dispatch(submitFormData(fieldValues))
.then(res => {
history.push('/home');
});
});
return (
<FormUI data={formData} onSubmit={handleSubmit} /> ) } ... 复制代码
useCallback
会返回被它包裹的函数的memorized版本,只要依赖项不变,memorized的函数就不会更新。利用这一特色咱们能够把useEffect
中要调用的逻辑使用useCallback
封装到外部,而后只须要在useEffect
的依赖项里添加memorized的函数,就能够正常运做了。
然而正如前文提到的,mapStateToProps
中的ownProps
参数一样会引发mapStateToProps
的从新调用,产生新的state props:
// 此函数在connected组件接收到new props时会被调用
function handleNewProps() {
// 声明mapStateToProps时若是使用了ownProps参数一样会产生新的stateProps!
if (mapStateToProps.dependsOnOwnProps)
stateProps = mapStateToProps(state, ownProps)
if (mapDispatchToProps.dependsOnOwnProps)
dispatchProps = mapDispatchToProps(dispatch, ownProps)
mergedProps = mergeProps(stateProps, dispatchProps, ownProps)
return mergedProps
}
复制代码
所以在这种方案中若是useEffect
有依赖这些state props的话仍是有可能形成依赖检查失效(好比说state props是引用类型)。
既然前面几种方案或多或少都有些坑点,那么不妨尝试一下React Redux在v7.1.0版本为咱们带来的官方hooks APIs,下面就展现下基本用法。
主要用到的API:
import { useSelector, useDispatch } from 'react-redux'
// selector函数的用法和mapStateToProps类似,其返回值会做为useSelector的返回值,但与mapStateToProps不一样的是,前者能够返回任何类型的值(而不止是一个对象),此外没有第二个参数ownProps(由于能够在组件内经过闭包拿到)
const result : any = useSelector(selector : Function, equalityFn? : Function)
const dispatch = useDispatch()
复制代码
使用:
...
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(() => {
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} /> ); } ... // 无需使用connect export default withRouter(React.memo(Form)); 复制代码
能够看到和上面介绍的"不使用mapDispatchToProps"
的方式很类似,都是经过传入dispatch
,而后把须要使用dispatch
的逻辑定义在组件内部,最大差别是把提取state
的地方从mapStateToProps
变成useSelector
。二者的用法相近,但若是你想后者像前者同样返回一个对象的话要特别注意:
因为useSelector
内部默认是使用===
来判断先后两次selector函数的计算结果是否相同的(若是不相同就会触发组件re-render),那么若是selector函数返回的是对象,那实际上每次useSelector
执行时调用它都会产生一个新对象,这就会形成组件无心义的re-render。要解决这个问题,可使用reselect等库建立带memoized效果的selector ,或者给useSelector
的第二个参数(比较函数)传入react-redux内置的shallowEqual
:
import { useSelector, shallowEqual } from 'react-redux'
const selector = state => ({
a: state.a,
b: state.b
});
const data = useSelector(selector, shallowEqual);
复制代码
自从Hooks
出现后,社区上一个比较热门的话题就是用Hooks
手撸一套全局状态管理,一种常见的方式以下:
相关Hooks
:useContext
,useReducer
实现:
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;
}
}
// 建立一个context
const Store = createContext(null);
// 做为全局state
const initState = {
headerColor: 'red',
contentColor: 'blue'
};
const App = () => {
const [state, dispatch] = useReducer(reducer, initState);
// 在根结点注入全局state和dispatch方法
return (
<Store.Provider value={{ state, dispatch }}>
<Header/>
<Content/>
</Store.Provider>
);
};
const Header = memo(() => {
// 拿到注入的全局state和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')}
/>
);
});
复制代码
上述代码经过context
,把一个全局的state
和派发actions
的dispatch
函数注入到被Provider
包裹的全部子元素中,再配合useReducer,看起来确实是个穷人版的Redux。
然而,上述代码其实有性能隐患:不管咱们点击<Header/>
仍是<Content/>
去派发一个action,最终结果是:
<Header/>
和<Content/>
都会被从新渲染!
由于很显然,它们俩都消费了同一个state(尽管都只消费了state的一部分),因此当这个全局的state被更新后,全部的Consumer天然也会被更新。
但咱们不是已经用
memo
包裹组件了吗?
是的,memo
能为咱们守住来自props的更新,然而state
是在组件内部经过useContext
这个hook注入的,这么一来就会绕过最外层的memo
。
那么有办法能够避免这种强制更新吗? Dan Abramov大神给咱们指了几条明路:
拆分Context(推荐)。把全局的State按需求拆分到不一样的context
,那么天然不会相互影响致使无谓的重渲染;
把组件拆成两个,里层的用memo
包裹:
const Header = () => {
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')}
/>
);
});
复制代码
使用useMemo
hook。思路其实跟上面的同样,但不用拆成两个组件:
const Header = () => {
const { state, dispatch } = useContext(Store);
return useMemo(
() => (
<header style={{backgroundColor: state.headerColor}} onClick={() => dispatch('UPDATE_HEADER_COLOR')} /> ), [state.headerColor, dispatch] ); }; 复制代码
可见,若是使用Context
+ Hooks
来代替Redux等状态管理工具,那么咱们必须花费额外的心思去避免性能问题,然而这些dirty works其实React-Redux等工具已经默默替咱们解决了。除此以外,咱们还会面临如下问题:
因此,除非是在对状态管理需求很简单的我的或技术项目里,或者纯粹想造轮子练练手,不然我的是不建议放弃Redux等成熟的状态管理方案的,由于性价比不高。
React Hooks给开发者带来了清爽的使用体验,必定程度上提高了键盘的寿命【并不,然而与原有的React-Redux connect
相关APIs结合使用时,须要特别当心ownProps
参数,很容易踩坑,建议尽快升级到v7.1.0版本,使用官方提供的Hooks API。
此外,使用Hooks自建全局状态管理的方式在小项目中当然可行,然而想用在较大型的、正式的业务中,至少还要花费心思解决性能问题,而这个问题正是React-Redux等工具已经花费很多功夫帮咱们解决了的,彷佛并无什么充分的理由要抛弃它们。
关注【IVWEB社区】公众号获取每周最新文章,通往人生之巅!