做者: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社區】公衆號獲取每週最新文章,通往人生之巔!