在離職以後,我開始靜下心來,思考原來在繁重的業務開發節奏中無暇思考的一些問題,本期的主題是純函數鉤子useReducer
和共享狀態鉤子useContext
。前端
在react中,reducer
函數是一個很重要的概念。它表示一個接收舊狀態,返回新狀態的函數。vue
const nums = [1, 2, 3]
const value = nums.reduce((acc, cur) => acc + cur, 0)
在上述例子中,reduce函數的一個參數,就是一個標準的reducer
函數。react
在以前的setState
使用中,你可能會好奇setNum(prev => prev + 1)
中prev
怎麼來的,讓咱們深刻到最底層看看吧,實際上useState
並不是最底層的元素,它內部仍然用到了useReducer
來實現,在react源碼中有個basicStateReducer
,大體結構以下:web
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
因此,當咱們的setter接收的參數是一個函數時,舊的state將做爲參數被該函數使用。vuex
useReducer
基本用法以下:數據庫
const [state, dispatch] = useReducer(reducer, initialState, initFunc);
其中第三個參數是可選參數,咱們通常只會用到前兩個。redux
一個簡單的示例(實現數字+1)以下:編輯器
import React, { useReducer } from "react";
const initialState = {
num: 0,
};
function reducer(state, action) {
switch (action.type) {
case "increase":
return { num: state.num + 1 };
case "decrease":
return { num: state.num - 1 };
default:
throw new Error();
}
}
const NumsDemo = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<h1>數字爲: {state.num}</h1>
<button onClick={() => dispatch({ type: "increase" })}>+</button>
<button onClick={() => dispatch({ type: "decrease" })}>-</button>
</>
);
};
export default NumsDemo;
你可能也發現了,useReducer
和 useState
很是相似:定義狀態和修改狀態的邏輯。useReducer
用起來更加麻煩,可是若是須要維護的狀態自己比較複雜,多個狀態之間相互依賴,那麼 useReducer
的好處才真正顯示出來了:用一個語義化的action
來隱藏修改狀態的複雜細節。ide
useContext
則比較簡單,它用於在局部組件樹中共享數據,有點相似於vue中的provide/inject
,基本使用以下:函數
// 在模塊的入口文件中定義
// Home.tsx
export const MyContext = React.createContext({num: 24});
// 在模塊內部的某個組件中獲取
// Sub.tsx
import MyContext from '../Home';
const Sub = () => {
const state = useContext(MyContext);
// ...
}
好了,如今咱們已經肯定要弄一個小型redux了,不過在這以前咱們仍是須要回顧一下redux的基本理念。
在對於小型頁面共享數據時,咱們通常會有諸如「狀態提高」這樣的開發約定,也就是說,咱們會將共享的狀態放到上層最近的公共父級。可是當頁面數量一多,組件拆分粒度變細,這種「共享狀態」的機制變得很脆弱,很冗餘。
redux 的機制就是爲了解決這個問題,redux 有幾個很是明顯的特色:
redux 的單項數據流,能夠歸納爲三個部分:View
,Reducers
,Store
。
View
視圖層發起更新動做(dispatch
),會抵達更新函數層(Reducers
),更新函數執行並返回最新狀態,抵達狀態層(Store
),狀態層更新後將通知視圖層更新。
redux
其實我以爲,不管是 vuex 也好,redux 也好,它的設計理念都是相似一個「前端數據庫」。在store
層應該只存放公共狀態,不建議存放其餘的東西,好比公共方法,由於這與reducer
純函數的理念是相悖的。
好了,讓一切開始吧,咱們這裏只定義三個組件:根組件App
,第一個子組件Sub1
,第二個子組件Sub2
。實現一個很是簡單的數字加減功能,以下:
// 說明:爲了代碼更加易讀,已經將ts的類型定義作了刪除操做
// App.tsx
import React, { useReducer } from "react";
import Sub1 from "./Sub1";
import Sub2 from "./Sub2";
const INITIAL_STATE = {
name: "zhang",
age: 24,
};
// 定義改變狀態的幾種操做
function reducer(state, action) {
switch (action.type) {
case "increaseAge":
return { ...state, age: state.age + 1 };
case "decreaseAge":
return { ...state, age: state.age - 1 };
case "changeName":
return { ...state, name: action.payload };
default:
return state;
}
}
// 選擇性導出該context
export const AppContext = React.createContext(null);
function App() {
const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
return (
<AppContext.Provider value={{ state, dispatch }}>
<Sub1 />
<Sub2 />
</AppContext.Provider>
);
}
export default App;
// 在Sub1.tsx中
import React, { useContext } from "react";
import { AppContext } from "./App";
const Sub1 = () => {
const { state, dispatch } = useContext(AppContext);
return (
<>
<h1>Sub1年齡爲: {state.age}, 名字爲:{state.name}</h1>
<button onClick={() => dispatch({ type: "increaseAge" })}>+</button>
</>
);
};
export default Sub1;
// 在Sub2.tsx中
import React, { useContext } from "react";
import { AppContext } from "./App";
const Sub2 = () => {
const { state, dispatch } = useContext(AppContext);
return (
<>
<h1>Sub2年齡爲: {state.age}, 名字爲:{state.name}</h1>
<button onClick={() => dispatch({ type: "decreaseAge" })}>-</button>
</>
);
};
export default Sub2;
運行以上示例,能夠發如今一處子組件更改公共狀態,其餘消費到該公共狀態的組件(Consumer
)都會更新。這有效避免了隔代傳props所引起的代碼臃腫脆弱問題。
不能,我在項目中雖然已經這麼用了,可是仍是發現對比redux的功能是有所欠缺的,其中最典型的就是更新公共狀態後沒有回調的問題。
useReducer
+ useContext
其實是製造了一個「窮人版的 redux」。並且咱們必須花費額外的心思去避免性能問題(React.memo
、useCallback
等),然而這些煩人的 dirty works 其實 React-Redux 等工具已經默默替咱們解決了。除此以外,咱們還會面臨如下問題:
以上四個坑點摘抄於騰訊的這篇文章,仔細讀完後發現確實寫的能夠:Redux with Hooks