React系列-Mixin、HOC、Render Props(上)
React系列-輕松學會Hooks(中)
React系列-自定義Hooks很簡單(下)
我們在第二篇文章中介紹了一些常用的hooks,接著我們繼續來介紹剩下的hooks吧
useReducer
作為useState 的替代方案。它接收一個形如
(state, action) => newState 的 reducer
,并返回當前的 state 以及與其配套的 dispatch 方法
。(如果你熟悉 Redux 的話,就已經知道它如何工作了。)
不明白Redux工作流的同學可以看看這篇Redux系列之分析中間件原理(附經驗分享)
為什么使用
官方說法: 在某些場景下,useReducer 會比 useState 更適用,
例如 state 邏輯較復雜且包含多個子值,或者下一個 state 依賴于之前的 state 等
。并且,使用 useReducer 還能給那些會觸發深更新的組件做性能優化
,因為你可以向子組件傳遞 dispatch 而不是回調函數 。
總結來說:
如果你的state是一個數組或者對象等復雜數據結構
如果你的state變化很復雜,
經常一個操作需要修改很多state
如果你希望構建自動化測試用例來保證程序的穩定性
如果你需要在深層子組件里面去修改一些狀態(也就是useReducer+useContext代替Redux)
如果你用應用程序比較大,希望UI和業務能夠分開維護
登錄場景
舉個例子?:
登錄場景
useState完成登錄場景
????function?LoginPage()?{
????????const?[name,?setName]?=?useState('');?//?用戶名
????????const?[pwd,?setPwd]?=?useState('');?//?密碼
????????const?[isLoading,?setIsLoading]?=?useState(false);?//?是否展示loading,發送請求中
????????const?[error,?setError]?=?useState('');?//?錯誤信息
????????const?[isLoggedIn,?setIsLoggedIn]?=?useState(false);?//?是否登錄
????????const?login?=?(event)?=>?{
????????????event.preventDefault();
????????????setError('');
????????????setIsLoading(true);
????????????login({?name,?pwd?})
????????????????.then(()?=>?{
????????????????????setIsLoggedIn(true);
????????????????????setIsLoading(false);
????????????????})
????????????????.catch((error)?=>?{
????????????????????//?登錄失敗:?顯示錯誤信息、清空輸入框用戶名、密碼、清除loading標識
????????????????????setError(error.message);
????????????????????setName('');
????????????????????setPwd('');
????????????????????setIsLoading(false);
????????????????});
????????}
????????return?(?
????????????//??返回頁面JSX?Element
????????)
????}
useReducer完成登錄場景
????const?initState?=?{
????????name:?'',
????????pwd:?'',
????????isLoading:?false,
????????error:?'',
????????isLoggedIn:?false,
????}
????function?loginReducer(state,?action)?{
????????switch(action.type)?{
????????????case?'login':
????????????????return?{
????????????????????...state,
????????????????????isLoading:?true,
????????????????????error:?'',
????????????????}
????????????case?'success':
????????????????return?{
????????????????????...state,
????????????????????isLoggedIn:?true,
????????????????????isLoading:?false,
????????????????}
????????????case?'error':
????????????????return?{
????????????????????...state,
????????????????????error:?action.payload.error,
????????????????????name:?'',
????????????????????pwd:?'',
????????????????????isLoading:?false,
????????????????}
????????????default:?
????????????????return?state;
????????}
????}
????function?LoginPage()?{
????????const?[state,?dispatch]?=?useReducer(loginReducer,?initState);
????????const?{?name,?pwd,?isLoading,?error,?isLoggedIn?}?=?state;
????????const?login?=?(event)?=>?{
????????????event.preventDefault();
????????????dispatch({?type:?'login'?});
????????????login({?name,?pwd?})
????????????????.then(()?=>?{
????????????????????dispatch({?type:?'success'?});
????????????????})
????????????????.catch((error)?=>?{
????????????????????dispatch({
????????????????????????type:?'error'
????????????????????????payload:?{?error:?error.message?}
????????????????????});
????????????????});
????????}
????????return?(?
????????????//??返回頁面JSX?Element
????????)
????}
??我們的state變化很復雜,經常一個操作需要修改很多state
,另一個好處是所有的state處理都集中到了一起,使得我們對state的變化更有掌控力,同時也更容易復用state邏輯變化代碼,比如在其他函數中也需要觸發登錄success狀態,只需要dispatch({ type: 'success' })。
筆者[狗頭]認為,暫時應該不會用useReducer
替代useState
,畢竟Redux
的寫法實在是很繁瑣
復雜數據結構場景
剛好最近筆者的項目就碰到了復雜數據結構場景
,可是并沒有用useReducer
來解決,依舊采用useState
,原因很簡單:方便
//?定義list類型
??export?interface?IDictList?extends?IList?{
??extChild:?{
????curPage:?number
????totalSize:?number
????size:?number?//?pageSize
????list:?IList[]
???}
?}
?const?[list,?setList]?=?useState([])const?change=()=>{const?datalist?=?JSON.parse(JSON.stringify(list))?//?拷貝對象?地址不同?不過這種寫法感覺不好?建議用reducers?應該封裝下reducers寫法const?data?=?await?getData()const?{?totalCount,?pageSize,?list?}?=?data
??????item.extChild.totalSize?=?totalCount
??????item.extChild.size?=?pageSize
??????item.extChild.list?=?list
??????setList(datalist)?//?改變
?}
看typescript寫的類型聲明就知道了這個list變量是個復雜的數據結構,需要經常需要改變添加extChild.list數組的內容,但是這種Array.prototype.push
,是不會觸發更新,做過是通過const datalist = JSON.parse(JSON.stringify(list))
。雖然沒有使用useReducer
進行替代,筆者還是推薦大家試試
如何使用
const?[state,?dispatch]?=?useReducer(reducer,?initialArg,?init);
知識點合集
引用不變
useReducer返回的state
跟ref一樣,引用是不變的,不會隨著函數組件的重新更新而變化,因此useReducer也可以解決閉包陷阱
const?setCountReducer?=?(state,action)=>{
??switch(action.type){
????case?'add':
??????return?state+action.value
????case?'minus':
??????return?state-action.value
????default:
??????return?state
??}
}
const?App?=?()=>{
??const?[count,dispatch]?=?useReducer(setCountReducer,0)
??useEffect(()=>{
????const?timeId?=?setInterval(()=>{
??????dispatch({type:'add',value:1})
????},1000)
????return?()=>?clearInterval(timeId)
??},[])
??return?(
????<span>{count}span>
??)
}
把setCount改成useReducer的dispatch,因為useReducer的dispatch 的身份永遠是穩定的 —— 即使 reducer 函數是定義在組件內部并且依賴 props
useContext
,
useContext
肯定與React.createContext有關系的,接收一個context 對象(React.createContext 的返回值)
并返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的的 value prop 決定。
為什么使用
如果你在接觸 Hook 前已經對 context API 比較熟悉,那應該可以理解,
useContext(MyContext) 相當于 class 組件中的 static contextType = MyContext 或者 。
簡單點說就是useContext
是用來消費context API
的
如何使用
const?value?=?useContext(MyContext);
知識點合集
useContext造成React.memo 無效
當組件上層最近的 更新時,該 Hook 會觸發重渲染,并使用最新傳遞給 MyContext provider 的 context value 值。即使
祖先使用 React.memo 或 shouldComponentUpdate
,??也會在組件本身使用 useContext 時重新渲染
。
舉個例子?:
//?創建一個?context
const?Context?=?React.createContext()
//?用memo包裹
const?Item?=?React.memo((props)?=>?{
??//?組件一,?useContext?寫法
??const?count?=?useContext(Context);
??console.log('props',?props)
??return?(
????<div>{count}div>
??)
})
const?App?=?()?=>?{
??const?[count,?setCount]?=?useState(0)
??return?(
????<div>
??????點擊次數:?{?count}<button?onClick={()?=>?{?setCount(count?+?1)?}}>點我button><Context.Provider?value={count}><Item?/>Context.Provider>div>
??)
}
結果:

可以看到即使props沒有變化,一旦組件上層最近的 更新時,該 Hook 會觸發重渲染
,此時Memo就失效了
Hooks替代Redux
有了
useReducer
和useContext
以及React.createContext
API,我們可以實現自己的狀態管理
來替換Redux
實現react-redux
react-redux:React Redux is the official React binding for Redux. It lets your React components read data from a Redux store, and dispatch actions to the store to update data.
簡單理解就是連接組件和數據中心,也就是把React和Redux聯系起來
,可以看看官方文檔或者看看阮一峰老師的文章,這里我們要去實現它最主要的兩個API
Provider 組件
Provider:組件之間共享的數據是 Provider 這個頂層組件通過 props 傳遞下去的,store必須作為參數放到Provider組件中去
利用
React.createContext
這個API,實現起來非常easy,react-redux
本身就是依賴這個API的
const?MyContext?=?React.createContext()
const?MyProvider?=?MyContext.Provider
export?default?MyProvider?//?導出
connect
connect:connect是一個高階組件,提供了一個連接功能,可用于將組件連接到store,它 提供了組件獲取 store 中數據或者更新數據的接口(mapStateToProps和mapStateToProps)
的能力
connect([mapStateToProps], [mapStateToProps], [mergeProps], [options])
function?connect(mapStateToProps,?mapDispatchToProps)?{
????return?function?(Component)?{
????????return?function?()?{
????????????const?{state,?dispatch}?=?useContext(MyContext)
????????????const?stateToProps?=?mapStateToProps(state)
????????????const?dispatchToProps?=?mapDispatchToProps(dispatch)
????????????const?props?=?{...props,?...stateToProps,?...dispatchToProps}
????????????return?(
????????????????<Component?{...props}?/>
????????????)
????????}
????}
}
export?default?connect?//?導出
創建store
store: store對象包含所有數據。如果想得到某個時點的數據,就要對 Store 生成快照。這種時點的數據集合,就叫做 State。
利用useReducer來創建我們的store
?import?React,?{?Component,?useReducer,?useContext?}?from?'react';
import?{?render?}?from?'react-dom';
import?'./style.css';
const?MyContext?=?React.createContext()
const?MyProvider?=?MyContext.Provider;
function?connect(mapStateToProps,?mapDispatchToProps)?{
????return?function?(Component)?{
????????return?function?()?{
????????????const?{state,?dispatch}?=?useContext(MyContext)
????????????const?stateToProps?=?mapStateToProps(state)
????????????const?dispatchToProps?=?mapDispatchToProps(dispatch)
????????????const?props?=?{...props,?...stateToProps,?...dispatchToProps}
????????????return?(
????????????????<Component?{...props}?/>
????????????)
????????}
????}
}
function?FirstC(props)?{
????console.log("FirstC更新")
????return?(<div><h2>這是FirstCh2><h3>{props.books}h3><button?onClick={()=>?props.dispatchAddBook("Dan?Brown:?Origin")}>Dispatch?'Origin'button>div>
????)
}
function?mapStateToProps(state)?{
????return?{
????????books:?state.Books
????}
}
function?mapDispatchToProps(dispatch)?{
????return?{
????????dispatchAddBook:?(payload)=>?dispatch({type:?'ADD_BOOK',?payload})
????}
}
const?HFirstC?=?connect(mapStateToProps,?mapDispatchToProps)(FirstC)
function?SecondC(props)?{
???console.log("SecondC更新")
????return?(<div><h2>這是SecondCh2><h3>{props.books}h3><button?onClick={()=>?props.dispatchAddBook("Dan?Brown:?The?Lost?Symbol")}>Dispatch?'The?Lost?Symbol'button>div>
????)
}
function?_mapStateToProps(state)?{
????return?{
????????books:?state.Books
????}
}
function?_mapDispatchToProps(dispatch)?{
????return?{
????????dispatchAddBook:?(payload)=>?dispatch({type:?'ADD_BOOK',?payload})
????}
}
const?HSecondC?=?connect(_mapStateToProps,?_mapDispatchToProps)(SecondC)
function?App?()?{
????const?initialState?=?{
??????Books:?'Dan?Brown:?Inferno'
????}
????const?[state,?dispatch]?=?useReducer((state,?action)?=>?{
??????switch(action.type)?{
????????case?'ADD_BOOK':
??????????return?{?Books:?action.payload?}
????????default:
??????????return?state
??????}
????},?initialState);
????return?(<div><MyProvider?value={{state,?dispatch}}><HFirstC?/><HSecondC?/>MyProvider>div>
????)
}
render(<App?/>,?document.getElementById('root'));
結果:

嗯嗯?,我們就這樣實現了一個狀態管理
缺陷
- 缺少時間旅行
- 不支持中間件
- 性能極差
可以看到上面的結果,一個狀態變化,所有組件都重新渲染
,嗯嗯?,所以我們這是個demo玩玩而已,不要用于生產中
最后貼下Redux作者的回答:

useLayoutEffect
useLayoutEffect和useEffect一樣也是處理副作用
,其函數簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調用 effect
。可以使用它來讀取 DOM 布局并同步觸發重渲染。在瀏覽器執行繪制之前
,useLayoutEffect 內部的更新計劃將被同步刷新。
??官方盡量推薦使用useEffect,因為useLayoutEffect,useLayoutEffect里面的callback函數會在DOM更新完成后立即執行
,但是會在瀏覽器進行任何繪制之前運行完成,阻塞了瀏覽器的繪制
區別就是:useEffect是異步的,useLayoutEffect是同步的
為什么使用
解決一些閃爍場景
如何使用
useLayoutEffect(fn,?[])?//?接收兩個參數?一個是回調函數?另外一個是數組類型的參數(表示依賴)
知識點合集
??暫無...
自定義hooks
自定義Hooks很簡單,利用官方提供的Hook我們可以把重用的邏輯抽離出來,也就是我們的自定義Hook,當你在一個項目中發現大量類似代碼,那就抽離成Hooks吧
??前面我們分析了Mixin,HOC,Render Props
這些模式來實現狀態邏輯復用
,這里的自定義hooks
也是解決狀態邏輯復用問題
的一種模式(?終于快完結了)
根據業務來說,我把自定義Hooks分為兩類,一類是自定義基礎Hooks
,另一類是自定義業務Hooks
業務Hooks
比如我們多個頁面有相同的請求方法
//?以use開頭
export?const?useUserData?=?(category:?string[],?labelName?:?string)?=>?{
??const?[baseTotal,?setBaseTotal]?=?useState(0)
??useEffect(()?=>?{
????dealSearchTotal(category,?labelName)
??},?[labelName,?category])const?dealSearchTotal?=?async?(
??)?=>?{const?data?=?await?getUserData(curCategory,?labelName)const?{?baseTotal,?calculateTotal,?basicTotal,?extTotal?}?=?data
????setBaseTotal(baseTotal)
??}//?最后return出想要的數據return?[baseTotal,?calculateTotal,?basicTotal,?extTotal]
}
??好如果你注意到你寫了重復代碼,抽離成自定義Hooks是沒問題的
基礎Hooks
基礎Hooks就是平時與業務無關的工具方法
useEffectOnce
該Hooks在函數組件只執行一次
const?useEffectOnce?=?(effect)?=>?{
??useEffect(effect,?[]);
};
export?default?useEffectOnce;
useMount
該Hook在組件掛載時調用
const?useMount?=?(fn)?=>?{
??useEffectOnce(()?=>?{
????fn();
??});
};
export?default?useMount;
useUnmount
該Hook在組件銷毀時調用
const?useUnmount?=?(fn:?()?=>?any):?void?=>?{
??const?fnRef?=?useRef(fn);
??fnRef.current?=?fn;
??useEffectOnce(()?=>?()?=>?fnRef.current());
};
export?default?useUnmount;
usePrevious
獲取組件的state或者props的舊值
const?usePrevious?=?(state):?=>?{
??const?ref?=?useRef();
??useEffect(()?=>?{
????ref.current?=?state;
??});
??return?ref.current;
};
export?default?usePrevious;
??其它參考Umi Hooks
最后
