原文:How to use useReducer in React Hooks for performance optimization html
github 的地址 歡迎 star!
React Hook 出來已經有一段時間了,具體的一些用法以及它解決的痛點,能夠查看 Dan 的兩篇文章 useEffect 完整指南以及編寫有彈性的組件進行詳細瞭解。react
本文主要是介紹了6種在 React Hooks 使用 useReducer 的不一樣的方法git
React Hooks API正式在 React V16.8 版本發佈了。這篇博客,主要是介紹了其中 useReducer 的各類用法示例。在讀以前,你確保你已經看過 React Hooks官方指南。github
useReducer hook 屬於官方擴展的 hooks:redux
是 useState 的另外一種替代。它接受
(state, action) => newState
,而且返回了一個與當前state成對的dispatch
的方法。(若是你熟悉 Redux ,你也很快就能理解它是怎麼工做的。)react-native
儘管 useReducer 是擴展的 hook, 而 useState 是基本的 hook,但 useState 實際上執行的也是一個 useReducer。這意味着 useReducer 是更原生的,你能在任何使用 useState 的地方都替換成使用 useReducer。Reducer 如此給力,因此有各類各樣的用例。瀏覽器
本文接下來就介紹了幾種表明性的用例。每一個例子都表明一種特定的用例,都有相關的代碼。性能優化
能夠看這個簡單示例的代碼。下文都是用這個計數的例子作延伸的。bash
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
複製代碼
首先,咱們定義了初始化的 initialState 以及 reducer。注意這裏的 state 僅是一個數字,不是對象。熟悉 Redux 的開發者多是困惑的,但在 hook 中是適宜的。此外,action 僅是一個普通的字符串。ide
下面是一個使用 useReducer 的組件。
const Example01 = () => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};
複製代碼
當用戶點擊一個按鈕,它就會 dispatch 一個 action 來更新計數值 count,頁面就會展現更新以後 count。你能夠在 reducer中儘量多定義 action,但這種模式有侷限,它的 action是有限的。
下面是完整的代碼:
import React, { useReducer } from 'react';
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'reset': return 0;
default: throw new Error('Unexpected action');
}
};
const Example01 = () => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<div>
{count}
<button onClick={() => dispatch('increment')}>+1</button>
<button onClick={() => dispatch('decrement')}>-1</button>
<button onClick={() => dispatch('reset')}>reset</button>
</div>
);
};
export default Example01;
複製代碼
這個例子 Redux的使用者是熟悉的。咱們使用了 state對象以及一個 action對象。
const initialState = {
count1: 0,
count2: 0,
};
const reducer = (state, action) => {
switch (action.type) {
case 'increment1':
return { ...state, count1: state.count1 + 1 };
case 'decrement1':
return { ...state, count1: state.count1 - 1 };
case 'set1':
return { ...state, count1: action.count };
case 'increment2':
return { ...state, count2: state.count2 + 1 };
case 'decrement2':
return { ...state, count2: state.count2 - 1 };
case 'set2':
return { ...state, count2: action.count };
default:
throw new Error('Unexpected action');
}
};
複製代碼
在 state中存放了兩個數字。咱們能使用複雜的對象表示 state,只要能把 reducer組織好(列如:react-redux中 combineReducers)。另外,由於 action是一個對象,除了 type值,你還能夠給它添加其餘屬性像action.count
。這個例子中 reducer 是有點雜亂的,但不妨礙咱們下面這樣使用它:
const Example02 = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<div>
{state.count1}
<button onClick={() => dispatch({ type: 'increment1' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement1' })}>-1</button>
<button onClick={() => dispatch({ type: 'set1', count: 0 })}>reset</button>
</div>
<div>
{state.count2}
<button onClick={() => dispatch({ type: 'increment2' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement2' })}>-1</button>
<button onClick={() => dispatch({ type: 'set2', count: 0 })}>reset</button>
</div>
</>
);
};
複製代碼
注意到 state 中有兩個計數器,定義各自相應的 action 類型來更新它們。在線示例點擊這裏
上面的單個 state 中出現兩個計數器,這就是一種典型的全局 state 的方法。但咱們僅僅須要使用本地(局部)的 state,故有另一種方法,可使用 useReducer 兩次。
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case 'increment': return state + 1;
case 'decrement': return state - 1;
case 'set': return action.count;
default: throw new Error('Unexpected action');
}
};
複製代碼
這裏的 state 是一個數字,而不是對象,它和用例1中是一致的。注意不一樣點這裏的 action 是一個對象。
組件如何使用呢
const Example03 = () => {
const [count1, dispatch1] = useReducer(reducer, initialState);
const [count2, dispatch2] = useReducer(reducer, initialState);
return (
<>
<div>
{count1}
<button onClick={() => dispatch1({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch1({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch1({ type: 'set', count: 0 })}>reset</button>
</div>
<div>
{count2}
<button onClick={() => dispatch2({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch2({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch2({ type: 'set', count: 0 })}>reset</button>
</div>
</>
);
};
複製代碼
能夠看到,每一個計數器有各自的 dispatch 的方法,但共享了 reducer 方法。這個的功能和用例2是一致的。
來看一個真實的例子,多個 useReducer 能各司其職。咱們以 React 原生的 input 輸入組件爲例,在本地狀態 state 中存儲文本數據。經過調用 dispatch 函數更新文本狀態值。
const initialState = '';
const reducer = (state, action) => action;
複製代碼
注意到每次調用 reducer,以前舊的 state 就會被丟掉。具體使用以下:
const Example04 = () => {
const [firstName, changeFirstName] = useReducer(reducer, initialState);
const [lastName, changeLastName] = useReducer(reducer, initialState);
return (
<>
<div>
First Name:
<TextInput value={firstName} onChangeText={changeFirstName} />
</div>
<div>
Last Name:
<TextInput value={lastName} onChangeText={changeLastName} />
</div>
</>
);
};
複製代碼
就是這麼簡單。固然你也能夠添加一些校驗的邏輯在裏面。完整代碼:
import React, { useReducer } from 'react';
const initialState = '';
const reducer = (state, action) => action;
const Example04 = () => {
const [firstName, changeFirstName] = useReducer(reducer, initialState);
const [lastName, changeLastName] = useReducer(reducer, initialState);
return (
<>
<div>
First Name:
<TextInput value={firstName} onChangeText={changeFirstName} />
</div>
<div>
Last Name:
<TextInput value={lastName} onChangeText={changeLastName} />
</div>
</>
);
};
// ref: https://facebook.github.io/react-native/docs/textinput
const TextInput = ({ value, onChangeText }) => (
<input type="text" value={value} onChange={e => onChangeText(e.target.value)} />
);
export default Example04;
複製代碼
有些時候,我但願在組件之間共享狀態(理解爲實現全局狀態 state )。一般,全局狀態會限制組件的複用,所以咱們首先考慮使用本地 state,經過 props 進行傳遞( dispatch 來改變),但當它不是那麼方便的時候(理解嵌套傳遞過多),就可使用 Context。若是你熟悉 Context 的 API,請點擊查看官方文檔。
這個例子,使用了和用例3同樣的 reducer。接下來看怎麼建立一個 context。
const CountContext = React.createContext();
const CountProvider = ({ children }) => {
const contextValue = useReducer(reducer, initialState);
return (
<CountContext.Provider value={contextValue}>
{children}
</CountContext.Provider>
);
};
const useCount = () => {
const contextValue = useContext(CountContext);
return contextValue;
};
複製代碼
useCount 就是自定義的 hook,它也和其餘官方的 hook 同樣使用。以下面同樣:
const Counter = () => {
const [count, dispatch] = useCount();
return (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
</div>
);
};
複製代碼
contextValue
就是 useReducer 返回的結果,咱們也用 hook 重構了useCount
。注意到這個點,使用了哪個 context是不固定的。
最後,像這樣使用 context:
const Example05 = () => (
<>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
</>
);
複製代碼
如上所示,有兩個 CountProvider
組件,意味着有兩個計數器,即便咱們只使用了一個 context。 在同一個 CountProvider
組件中計數器共享狀態 state。你能夠運行一下這個用例瞭解它是怎麼工做的。點擊這裏查看
在hooks中實現組件共享狀態 state 首選的應該就是 Context,但當在 React 組件的外部早已經有一個共享狀態 state 時,該怎麼(共享呢)?專業的作法訂閱監聽狀態 state,當共享狀態 state 更新時,更新組件。固然它還有一些侷限性,不過React官方提供了一個公共功能 create-subscription,你能夠用它來進行訂閱。
不幸的,這個公共方法包尚未用 React Hooks 進行重寫,如今只能靠咱們本身用 hooks 盡力去實現。讓咱們不使用 Context 實現和用例 5 同樣的功能。
首先,建立一個自定義的hook:
const useForceUpdate = () => useReducer(state => !state, false)[1];
複製代碼
這個 reducer 僅僅是對先前的 state 取反,忽略了 action。[1]
僅僅返回了 dispatch 而沒有 state。接下來,主函數實現共享狀態 state 以及返回自定義 hook:
const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach(callback => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
callback(); // in case it's already updated const cleanup = () => { const index = subscribers.indexOf(callback); subscribers.splice(index, 1); }; return cleanup; }, []); return [state, dispatch]; }; return useSharedState; }; 複製代碼
咱們使用了 useEffect。它是很是重要的 hook,你須要仔細的看官方文檔學習如何使用它。在 useEffect 中,咱們訂閱了一個回調函數來強制更新組件。在組件銷燬的時候須要清除訂閱。
接下來,咱們能夠建立兩個共享狀態 state。使用了和用例5,用例3同樣的 reducer 和初始值 initialState:
const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
複製代碼
這和用例 5 是不同的,這兩個 hooks 綁定了特定的共享狀態 state。而後咱們使用這兩個 hooks。
const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'set', count: 0 })}>reset</button>
</div>
);
const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />
};
const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />
};
複製代碼
注意到,Counter 組件是一個共同的無狀態組件。這樣子使用:
const Example06 = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);
複製代碼
能夠看到,咱們沒有用 Context,可是也實現了共享狀態。你們應該都要具體看看 useReducer,對於性能優化頗有幫助。
- hooks每次 Render 都有本身的 Props 與 State 能夠認爲每次 Render 的內容都會造成一個快照並保留下來(函數被銷燬了,但變量被react保留下來了),所以當狀態變動而 Rerender 時,就造成了 N 個 Render 狀態,而每一個 Render 狀態都擁有本身固定不變的 Props 與 State。 這也是函數式的特性--數據不變性
- 性能注意事項 useState 函數的參數雖然是初始值,但因爲整個函數都是 Render,所以每次初始化都會被調用,若是初始值計算很是消耗時間,建議使用函數傳入,這樣只會執行一次:
- 若是你熟悉 React 的 class 組件的生命週期,你能夠認爲
useEffect Hook
就是組合了componentDidMount, componentDidUpdate, 以及 componentWillUnmount(在useEffect的回調中)
,可是又有區別,useEffect不會阻止瀏覽器更新屏幕- hooks 把相關的邏輯放在一塊兒統一處理,不在按生命週期把邏輯拆分開
- useEffect 是在瀏覽器 render 以後觸發的,想要實現 DOM 改變時同步觸發的話,得用 useLayoutEffect,在瀏覽器重繪以前和 DOM 改變一塊兒,同步觸發的。不過儘可能在佈局的時候,其餘的用標準的 useEffect,這樣能夠避免阻止視圖更新。
```
function FunctionComponent(props) {
const [rows, setRows] = useState(() => createRows(props.count));
}
useRef 不支持這種特性,須要寫一些[冗餘的函斷定是否進行過初始化。](https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily)
```
複製代碼
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!