React Hooks

一篇看懂 React Hooks

將以前對 React Hooks 的總結整理在一篇文章,帶你從認識到使用 React Hooks。css

github:https://github.com/ascoders

什麼是 React Hooks

React Hooks 是 React 16.7.0-alpha 版本推出的新特性,想嘗試的同窗安裝此版本便可。html

React Hooks 要解決的問題是狀態共享,是繼 render-props 和 higher-order components 以後的第三種狀態共享方案,不會產生 JSX 嵌套地獄問題。前端

這個狀態指的是狀態邏輯,因此稱爲狀態邏輯複用會更恰當,由於只共享數據處理邏輯,不會共享數據自己。react

不久前精讀分享過的一篇 Epitath 源碼 - renderProps 新用法 就是解決 JSX 嵌套問題,有了 React Hooks 以後,這個問題就被官方正式解決了。git

爲了更快理解 React Hooks 是什麼,先看筆者引用的下面一段 renderProps 代碼:github

function App() {
return (
<Toggle initial={false}>
{({ on, toggle }) => (
<Button type="primary" onClick={toggle}> Open Modal </Button>
<Modal visible={on} onOk={toggle} onCancel={toggle} />
)}
</Toggle>
)
}

恰巧,React Hooks 解決的也是這個問題:spring

function App() {
const [open, setOpen] = useState(false);
return (
<>
<Button type="primary" onClick={() => setOpen(true)}>
Open Modal
</Button>
<Modal
visible={open}
onOk={() => setOpen(false)}
onCancel={() => setOpen(false)}
/>
</>
);
}

能夠看到,React Hooks 就像一個內置的打平 renderProps 庫,咱們能夠隨時建立一個值,與修改這個值的方法。看上去像 function 形式的 setState,其實這等價於依賴注入,與使用 setState 相比,這個組件是沒有狀態的。json

React Hooks 的特色

React Hooks 帶來的好處不只是 「更 FP,更新粒度更細,代碼更清晰」,還有以下三個特性:redux

  1. 多個狀態不會產生嵌套,寫法仍是平鋪的(renderProps 能夠經過 compose 解決,可不但使用略爲繁瑣,並且由於強制封裝一個新對象而增長了實體數量)。數組

  2. Hooks 能夠引用其餘 Hooks。

  3. 更容易將組件的 UI 與狀態分離。

第二點展開說一下:Hooks 能夠引用其餘 Hooks,咱們能夠這麼作:

import { useState, useEffect } from "react";

// 底層 Hooks, 返回布爾值:是否在線
function useFriendStatusBoolean(friendID) {
const [isOnline, setIsOnline] = useState(null);

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

return isOnline;
}

// 上層 Hooks,根據在線狀態返回字符串:Loading... or Online or Offline
function useFriendStatusString(props) {
const isOnline = useFriendStatusBoolean(props.friend.id);

if (isOnline === null) {
return "Loading...";
}
return isOnline ? "Online" : "Offline";
}

// 使用了底層 Hooks 的 UI
function FriendListItem(props) {
const isOnline = useFriendStatusBoolean(props.friend.id);

return (
<li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li>
);
}

// 使用了上層 Hooks 的 UI
function FriendListStatus(props) {
const statu = useFriendStatusString(props.friend.id);

return <li>{statu}</li>;
}

這個例子中,有兩個 Hooks:useFriendStatusBoolean 與 useFriendStatusStringuseFriendStatusString 是利用 useFriendStatusBoolean 生成的新 Hook,這兩個 Hook 能夠給不一樣的 UI:FriendListItemFriendListStatus 使用,而由於兩個 Hooks 數據是聯動的,所以兩個 UI 的狀態也是聯動的。

順帶一提,這個例子也能夠用來理解 對 React Hooks 的一些思考 一文的那句話:「有狀態的組件沒有渲染,有渲染的組件沒有狀態」:

  • useFriendStatusBoolean 與 useFriendStatusString 是有狀態的組件(使用 useState),沒有渲染(返回非 UI 的值),這樣就能夠做爲 Custom Hooks 被任何 UI 組件調用。

  • FriendListItem 與 FriendListStatus 是有渲染的組件(返回了 JSX),沒有狀態(沒有使用 useState),這就是一個純函數 UI 組件,

利用 useState 建立 Redux

Redux 的精髓就是 Reducer,而利用 React Hooks 能夠輕鬆建立一個 Redux 機制:

// 這就是 Redux
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);

function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}

return [state, dispatch];
}

這個自定義 Hook 的 value 部分看成 redux 的 state,setValue 部分看成 redux 的 dispatch,合起來就是一個 redux。而 react-redux 的 connect 部分作的事情與 Hook 調用同樣:

// 一個 Action
function useTodos() {
const [todos, dispatch] = useReducer(todosReducer, []);

function handleAddClick(text) {
dispatch({ type: "add", text });
}

return [todos, { handleAddClick }];
}

// 綁定 Todos 的 UI
function TodosUI() {
const [todos, actions] = useTodos();
return (
<>
{todos.map((todo, index) => (
<div>{todo.text}</div>
))}
<button onClick={actions.handleAddClick}>Add Todo</button>
</>
);
}

useReducer 已經做爲一個內置 Hooks 了,在這裏能夠查閱全部 內置 Hooks。

不過這裏須要注意的是,每次 useReducer 或者本身的 Custom Hooks 都不會持久化數據,因此好比咱們建立兩個 App,App1 與 App2:

function App1() {
const [todos, actions] = useTodos();

return <span>todo count: {todos.length}</span>;
}

function App2() {
const [todos, actions] = useTodos();

return <span>todo count: {todos.length}</span>;
}

function All() {
return (
<>
<App1 />
<App2 />
</>
);
}

這兩個實例同時渲染時,並非共享一個 todos 列表,而是分別存在兩個獨立 todos 列表。也就是 React Hooks 只提供狀態處理方法,不會持久化狀態。

若是要真正實現一個 Redux 功能,也就是全局維持一個狀態,任何組件 useReducer 都會訪問到同一份數據,能夠和 useContext 一塊兒使用。

大致思路是利用 useContext 共享一份數據,做爲 Custom Hooks 的數據源。具體實現能夠參考 redux-react-hook。

利用 useEffect 代替一些生命週期

在 useState 位置附近,可使用 useEffect 處理反作用:

useEffect(() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
subscription.unsubscribe();
};
});

useEffect 的代碼既會在初始化時候執行,也會在後續每次 rerender 時執行,而返回值在析構時執行。這個更多帶來的是便利,對比一下 React 版 G2 調用流程:

class Component extends React.PureComponent<Props, State> {
private chart: G2.Chart = null;
private rootDomRef: React.ReactInstance = null;

componentDidMount() {
this.rootDom = ReactDOM.findDOMNode(this.rootDomRef) as HTMLDivElement;

this.chart = new G2.Chart({
container: document.getElementById("chart"),
forceFit: true,
height: 300
});
this.freshChart(this.props);
}

componentWillReceiveProps(nextProps: Props) {
this.freshChart(nextProps);
}

componentWillUnmount() {
this.chart.destroy();
}

freshChart(props: Props) {
// do something
this.chart.render();
}

render() {
return <div ref={ref => (this.rootDomRef = ref)} />;
}
}

用 React Hooks 能夠這麼作:

function App() {
const ref = React.useRef(null);
let chart: G2.Chart = null;

React.useEffect(() => {
if (!chart) {
chart = new G2.Chart({
container: ReactDOM.findDOMNode(ref.current) as HTMLDivElement,
width: 500,
height: 500
});
}

// do something
chart.render();

return () => chart.destroy();
});

return <div ref={ref} />;
}

能夠看到將細碎的代碼片斷結合成了一個完整的代碼塊,更維護。

如今介紹了 useState useContext useEffect useRef 等經常使用 hooks,更多能夠查閱:內置 Hooks,相信不久的將來,這些 API 又會成爲一套新的前端規範。

React Hooks 將帶來什麼變化

Hooks 帶來的約定

Hook 函數必須以 "use" 命名開頭,由於這樣才方便 eslint 作檢查,防止用 condition 判斷包裹 useHook 語句。

爲何不能用 condition 包裹 useHook 語句,詳情能夠見 官方文檔,這裏簡單介紹一下。

React Hooks 並非經過 Proxy 或者 getters 實現的(具體能夠看這篇文章 React hooks: not magic, just arrays),而是經過數組實現的,每次 useState 都會改變下標,若是 useState 被包裹在 condition 中,那每次執行的下標就可能對不上,致使 useState 導出的 setter 更新錯數據。

雖然有 eslint-plugin-react-hooks 插件保駕護航,但這第一次將 「約定優先」 理念引入了 React 框架中,帶來了史無前例的代碼命名和順序限制(函數命名遭到官方限制,JS 自由主義者也許會暴跳如雷),但帶來的便利也是史無前例的(沒有比 React Hooks 更好的狀態共享方案了,約定帶來提效,自由的代價就是回到 renderProps or HOC,各團隊能夠自行評估)。

筆者認爲,React Hooks 的誕生,也許來自於這個靈感:「不如經過增長一些約定,完全解決狀態共享問題吧!」

React 約定大於配置腳手架 nextjs umi 以及筆者的 pri 都經過有 「約定路由」 的功能,大大下降了路由配置複雜度,那麼 React Hooks 就像代碼級別的約定,大大下降了代碼複雜度。

狀態與 UI 的界限會愈來愈清晰

由於 React Hooks 的特性,若是一個 Hook 不產生 UI,那麼它能夠永遠被其餘 Hook 封裝,雖然容許有反作用,可是被包裹在 useEffect 裏,整體來講仍是挺函數式的。而 Hooks 要集中在 UI 函數頂部寫,也很容易養成書寫無狀態 UI 組件的好習慣,踐行 「狀態與 UI 分開」 這個理念會更容易。

不過這個理念稍微有點蹩腳的地方,那就是 「狀態」 究竟是什麼。

function App() {
const [count, setCount] = useCount();
return <span>{count}</span>;
}

咱們知道 useCount 算是無狀態的,由於 React Hooks 本質就是 renderProps 或者 HOC 的另外一種寫法,換成 renderProps 就好理解了:

<Count>{(count, setCount) => <App count={count} setCount={setCount} />}</Count>;

function App(props) {
return <span>{props.count}</span>;
}

能夠看到 App 組件是無狀態的,輸出徹底由輸入(Props)決定。

那麼有狀態無 UI 的組件就是 useCount 了:

function useCount() {
const [count, setCount] = useState(0);
return [count, setCount];
}

有狀態的地方應該指 useState(0) 這句,不過這句和無狀態 UI 組件 App 的 useCount() 很像,既然 React 把 useCount 成爲自定義 Hook,那麼 useState 就是官方 Hook,具備同樣的定義,所以能夠認爲 useCount 是無狀態的,useState 也是一層 renderProps,最終的狀態實際上是 useState 這個 React 內置的組件。

咱們看 renderProps 嵌套的表達:

<UseState>
{(count, setCount) => (
<UseCount>
{" "}
{/**雖然是透傳,但給 count 作了去重,不可謂沒有做用 */}
{(count, setCount) => <App count={count} setCount={setCount} />}
</UseCount>
)}
</UseState>

能肯定的是,App 必定有 UI,而上面兩層父級組件必定沒有 UI。爲了最佳實踐,咱們儘可能避免 App 本身維護狀態,而其父級的 RenderProps 組件能夠維護狀態(也能夠不維護狀態,作個二傳手)。所以能夠考慮在 「有狀態的組件沒有渲染,有渲染的組件沒有狀態」 這句話後面加一句:沒渲染的組件也能夠沒狀態。

React Hooks 實踐

經過上面的理解,你已經對 React Hooks 有了基本理解,也許你也看了 React Hooks 基本實現剖析(就是數組),但理解實現原理就能夠用好了嗎?學的是知識,而用的是技能,看別人的用法就像刷抖音同樣(哇,飯還能夠這樣吃?),你總會有新的收穫。

首先,站在使用角度,要理解 React Hooks 的特色是 「很是方便的 Connect 一切」,因此不管是數據流、Network,或者是定時器均可以監聽,有一點 RXJS 的意味,也就是你能夠利用 React Hooks,將 React 組件打形成:任何事物的變化都是輸入源,當這些源變化時會從新觸發 React 組件的 render,你只須要挑選組件綁定哪些數據源(use 哪些 Hooks),而後只管寫 render 函數就好了!

DOM 反作用修改 / 監聽

作一個網頁,總有一些看上去和組件關係不大的麻煩事,好比修改頁面標題(切換頁面記得改爲默認標題)、監聽頁面大小變化(組件銷燬記得取消監聽)、斷網時提示(一層層裝飾器要堆成小山了)。而 React Hooks 特別擅長作這些事,造這種輪子,大小皆宜。

因爲 React Hooks 下降了高階組件使用成本,那麼一套生命週期才能完成的 「雜耍」 將變得很是簡單。

下面舉幾個例子:

修改頁面 title

效果:在組件裏調用 useDocumentTitle 函數便可設置頁面標題,且切換頁面時,頁面標題重置爲默認標題 「前端精讀」。

useDocumentTitle("我的中心");

實現:直接用 document.title 賦值,不能再簡單。在銷燬時再次給一個默認標題便可,這個簡單的函數能夠抽象在項目工具函數裏,每一個頁面組件都須要調用。

function useDocumentTitle(title) {
useEffect(
() => {
document.title = title;
return () => (document.title = "前端精讀");
},
[title]
);
}

在線 Demo

監聽頁面大小變化,網絡是否斷開

效果:在組件調用 useWindowSize 時,能夠拿到頁面大小,而且在瀏覽器縮放時自動觸發組件更新。

const windowSize = useWindowSize();
return <div>頁面高度:{windowSize.innerWidth}</div>;

實現:和標題思路基本一致,此次從 window.innerHeight 等 API 直接拿到頁面寬高便可,注意此時能夠用 window.addEventListener('resize') 監聽頁面大小變化,此時調用 setValue 將會觸發調用自身的 UI 組件 rerender,就是這麼簡單!

最後注意在銷燬時,removeEventListener 註銷監聽。

function getSize() {
return {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
outerHeight: window.outerHeight,
outerWidth: window.outerWidth
};
}

function useWindowSize() {
let [windowSize, setWindowSize] = useState(getSize());

function handleResize() {
setWindowSize(getSize());
}

useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);

return windowSize;
}

在線 Demo

動態注入 css

效果:在頁面注入一段 class,而且當組件銷燬時,移除這個 class。

const className = useCss({
color: "red"
});

return <div className={className}>Text.</div>;

實現:能夠看到,Hooks 方便的地方是在組件銷燬時移除反作用,因此咱們能夠安心的利用 Hooks 作一些反作用。注入 css 天然沒必要說了,而銷燬 css 只要找到注入的那段引用進行銷燬便可,具體能夠看這個 代碼片斷。

DOM 反作用修改 / 監聽場景有一些現成的庫了,從名字上就能看出來用法:document-visibility、network-status、online-status、window-scroll-position、window-size、document-title。

組件輔助

Hooks 還能夠加強組件能力,好比拿到並監聽組件運行時寬高等。

獲取組件寬高

效果:經過調用 useComponentSize 拿到某個組件 ref 實例的寬高,而且在寬高變化時,rerender 並拿到最新的寬高。

const ref = useRef(null);
let componentSize = useComponentSize(ref);

return (
<>
{componentSize.width}
<textArea ref={ref} />
</>
);

實現:和 DOM 監聽相似,此次換成了利用 ResizeObserver 對組件 ref 進行監聽,同時在組件銷燬時,銷燬監聽。

其本質仍是監聽一些反作用,但經過 ref 的傳遞,咱們能夠對組件粒度進行監聽和操做了。

useLayoutEffect(() => {
handleResize();

let resizeObserver = new ResizeObserver(() => handleResize());
resizeObserver.observe(ref.current);

return () => {
resizeObserver.disconnect(ref.current);
resizeObserver = null;
};
}, []);

在線 Demo,對應組件 component-size。

拿到組件 onChange 拋出的值

效果:經過 useInputValue() 拿到 Input 框當前用戶輸入的值,而不是手動監聽 onChange 再騰一個 otherInputValue 和一個回調函數把這一堆邏輯寫在無關的地方。

let name = useInputValue("Jamie");
// name = { value: 'Jamie', onChange: [Function] }
return <input {...name} />;

能夠看到,這樣不只沒有佔用組件本身的 state,也不須要手寫 onChange 回調函數進行處理,這些處理都壓縮成了一行 use hook。

實現:讀到這裏應該大體能夠猜到了,利用 useState 存儲組件的值,並拋出 value 與 onChange,監聽 onChange 並經過 setValue 修改 value, 就能夠在每次 onChange 時觸發調用組件的 rerender 了。

function useInputValue(initialValue) {
let [value, setValue] = useState(initialValue);
let onChange = useCallback(function(event) {
setValue(event.currentTarget.value);
}, []);

return {
value,
onChange
};
}

這裏要注意的是,咱們對組件加強時,組件的回調通常不須要銷燬監聽,並且僅需監聽一次,這與 DOM 監聽不一樣,所以大部分場景,咱們須要利用 useCallback 包裹,並傳一個空數組,來保證永遠只監聽一次,並且不須要在組件銷燬時註銷這個 callback。

在線 Demo,對應組件 input-value。

作動畫

利用 React Hooks 作動畫,通常是拿到一些具備彈性變化的值,咱們能夠將值賦給進度條之類的組件,這樣其進度變化就符合某種動畫曲線。

在某個時間段內獲取 0-1 之間的值

這個是動畫最基本的概念,某個時間內拿到一個線性增加的值。

效果:經過 useRaf(t) 拿到 t 毫秒內不斷刷新的 0-1 之間的數字,期間組件會不斷刷新,但刷新頻率由 requestAnimationFrame 控制(不會卡頓 UI)。

const value = useRaf(1000);

實現:寫起來比較冗長,這裏簡單描述一下。利用 requestAnimationFrame 在給定時間內給出 0-1 之間的值,那每次刷新時,只要判斷當前刷新的時間點佔總時間的比例是多少,而後作分母,分子是 1 便可。

在線 Demo,對應組件 use-raf。

彈性動畫

效果:經過 useSpring 拿到動畫值,組件以固定頻率刷新,而這個動畫值以彈性函數進行增減。

實際調用方式通常是,先經過 useState 拿到一個值,再經過動畫函數包住這個值,這樣組件就會從本來的刷新一次,變成刷新 N 次,拿到的值也隨着動畫函數的規則變化,最後這個值會穩定到最終的輸入值(如例子中的 50)。

const [target, setTarget] = useState(50);
const value = useSpring(target);

return <div onClick={() => setTarget(100)}>{value}</div>;

實現:爲了實現動畫效果,須要依賴 rebound 庫,它能夠實現將一個目標值拆解爲符合彈性動畫函數過程的功能,那咱們須要利用 React Hooks 作的就是在第一次接收到目標值是,調用 spring.setEndValue 來觸發動畫事件,並在 useEffect 裏作一次性監聽,再值變時從新 setValue便可。

最神奇的 setTarget 聯動 useSpring 從新計算彈性動畫部分,是經過 useEffect 第二個參數實現的:

useEffect(
() => {
if (spring) {
spring.setEndValue(targetValue);
}
},
[targetValue]
);

也就是當目標值變化後,纔會進行新的一輪 rerender,因此 useSpring 並不須要監聽調用處的 setTarget,它只須要監聽 target 的變化便可,而巧妙利用 useEffect 的第二個參數能夠事半功倍。

在線 Demo

Tween 動畫

明白了彈性動畫原理,Tween 動畫就更簡單了。

效果:經過 useTween 拿到一個從 0 變化到 1 的值,這個值的動畫曲線是 tween。能夠看到,因爲取值範圍是固定的,因此咱們不須要給初始值了。

const value = useTween();

實現:經過 useRaf 拿到一個線性增加的值(區間也是 0 ~ 1),再經過 easing 庫將其映射到 0 ~ 1 到值便可。這裏用到了 hook 調用 hook 的聯動(經過 useRaf 驅動 useTween),還能夠在其餘地方觸類旁通。

const fn: Easing = easing[easingName];
const t = useRaf(ms, delay);

return fn(t);

發請求

利用 Hooks,能夠將任意請求 Promise 封裝爲帶有標準狀態的對象:loading、error、result。

通用 Http 封裝

效果:經過 useAsync 將一個 Promise 拆解爲 loading、error、result 三個對象。

const { loading, error, result } = useAsync(fetchUser, [id]);

實現:在 Promise 的初期設置 loading,結束後設置 result,若是出錯則設置 error,這裏能夠將請求對象包裝成 useAsyncState 來處理,這裏就不放出來了。

export function useAsync(asyncFunction) {
const asyncState = useAsyncState(options);

useEffect(() => {
const promise = asyncFunction();
asyncState.setLoading();
promise.then(
result => asyncState.setResult(result);,
error => asyncState.setError(error);
);
}, params);
}

具體代碼能夠參考 react-async-hook,這個功能建議僅瞭解原理,具體實現由於有一些邊界狀況須要考慮,好比組件 isMounted 後才能相應請求結果。

Request Service

業務層通常會抽象一個 request service 作統一取數的抽象(好比統一 url,或者能夠統一換 socket 實現等等)。假如之前比較 low 的作法是:

async componentDidMount() {
// setState: 改 isLoading state
try {
const data = await fetchUser()
// setState: 改 isLoading、error、data
} catch (error) {
// setState: 改 isLoading、error
}
}

後來把請求放在 redux 裏,經過 connect 注入的方式會稍微有些改觀:

@Connect(...)
class App extends React.PureComponent {
public componentDidMount() {
this.props.fetchUser()
}

public render() {
// this.props.userData.isLoading | error | data
}
}

最後會發現仍是 Hooks 簡潔明瞭:

function App() {
const { isLoading, error, data } = useFetchUser();
}

而 useFetchUser 利用上面封裝的 useAsync 能夠很容易編寫:

const fetchUser = id =>
fetch(`xxx`).then(result => {
if (result.status !== 200) {
throw new Error("bad status = " + result.status);
}
return result.json();
});

function useFetchUser(id) {
const asyncFetchUser = useAsync(fetchUser, id);
return asyncUser;
}

填表單

React Hooks 特別適合作表單,尤爲是 antd form 若是支持 Hooks 版,那用起來會方便許多:

function App() {
const { getFieldDecorator } = useAntdForm();

return (
<Form onSubmit={this.handleSubmit} className="login-form">
<FormItem>
{getFieldDecorator("userName", {
rules: [{ required: true, message: "Please input your username!" }]
})(
<Input
prefix={<Icon type="user" style={{ color: "rgba(0,0,0,.25)" }} />}
placeholder="Username"
/>
)}
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit" className="login-form-button">
Log in
</Button>
Or <a href="">register now!</a>
</FormItem>
</Form>
);
}

不過雖然如此,getFieldDecorator 仍是基於 RenderProps 思路的,完全的 Hooks 思路是利用以前說的 組件輔助方式,提供一個組件方法集,用解構方式傳給組件。

Hooks 思惟的表單組件

效果:經過 useFormState 拿到表單值,而且提供一系列 組件輔助 方法控制組件狀態。

const [formState, { text, password }] = useFormState();
return (
<form>
<input {...text("username")} required />
<input {...password("password")} required minLength={8} />
</form>
);

上面能夠經過 formState 隨時拿到表單值,和一些校驗信息,經過 password("pwd") 傳給 input 組件,讓這個組件達到受控狀態,且輸入類型是 password 類型,表單 key 是 pwd。並且能夠看到使用的 form 是原生標籤,這種表單加強是至關解耦的。

實現:仔細觀察一下結構,不難發現,咱們只要結合 組件輔助 小節說的 「拿到組件 onChange 拋出的值」 一節的思路,就能輕鬆理解 textpassword 是如何做用於 input 組件,並拿到其輸入狀態。

往簡單的來講,只要把這些狀態 Merge 起來,經過 useReducer 聚合到 formState 就能夠實現了。

爲了簡化,咱們只考慮對 input 的加強,源碼僅需 30 幾行:

export function useFormState(initialState) {
const [state, setState] = useReducer(stateReducer, initialState || {});

const createPropsGetter = type => (name, ownValue) => {
const hasOwnValue = !!ownValue;
const hasValueInState = state[name] !== undefined;

function setInitialValue() {
let value = "";
setState({ [name]: value });
}

const inputProps = {
name, // 給 input 添加 type: text or password
get value() {
if (!hasValueInState) {
setInitialValue(); // 給初始化值
}
return hasValueInState ? state[name] : ""; // 賦值
},
onChange(e) {
let { value } = e.target;
setState({ [name]: value }); // 修改對應 Key 的值
}
};

return inputProps;
};

const inputPropsCreators = ["text", "password"].reduce(
(methods, type) => ({ ...methods, [type]: createPropsGetter(type) }),
{}
);

return [
{ values: state }, // formState
inputPropsCreators
];
}

上面 30 行代碼實現了對 input 標籤類型的設置,監聽 value onChange,最終聚合到大的 values做爲 formState 返回。讀到這裏應該發現對 React Hooks 的應用都是萬變不離其宗的,特別是對組件信息的獲取,經過解構方式來作,Hooks 內部再作一下聚合,就完成表單組件基本功能了。

實際上一個完整的輪子還須要考慮 checkbox radio 的兼容,以及校驗問題,這些思路大同小異,具體源碼能夠看 react-use-form-state。

模擬生命週期

有的時候 React15 的 API 仍是挺有用的,利用 React Hooks 幾乎能夠模擬出全套。

componentDidMount

效果:經過 useMount 拿到 mount 週期才執行的回調函數。

useMount(() => {
// quite similar to `componentDidMount`
});

實現:componentDidMount 等價於 useEffect 的回調(僅執行一次時),所以直接把回調函數拋出來便可。

useEffect(() => void fn(), []);

componentWillUnmount

效果:經過 useUnmount 拿到 unmount 週期才執行的回調函數。

useUnmount(() => {
// quite similar to `componentWillUnmount`
});

實現:componentWillUnmount 等價於 useEffect 的回調函數返回值(僅執行一次時),所以直接把回調函數返回值拋出來便可。

useEffect(() => fn, []);

componentDidUpdate

效果:經過 useUpdate 拿到 didUpdate 週期才執行的回調函數。

useUpdate(() => {
// quite similar to `componentDidUpdate`
});

實現:componentDidUpdate 等價於 useMount 的邏輯每次執行,除了初始化第一次。所以採用 mouting flag(判斷初始狀態)+ 不加限制參數確保每次 rerender 都會執行便可。

const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});

Force Update

效果:這個最有意思了,我但願拿到一個函數 update,每次調用就強制刷新當前組件。

const update = useUpdate();

實現:咱們知道 useState 下標爲 1 的項是用來更新數據的,並且就算數據沒有變化,調用了也會刷新組件,因此咱們能夠把返回一個沒有修改數值的 setValue,這樣它的功能就僅剩下刷新組件了。

const useUpdate = () => useState(0)[1];

對於 getSnapshotBeforeUpdategetDerivedStateFromErrorcomponentDidCatch 目前 Hooks 是沒法模擬的。

isMounted

好久之前 React 是提供過這個 API 的,後來移除了,緣由是能夠經過 componentWillMount 和 componentWillUnmount 推導。自從有了 React Hooks,支持 isMount 簡直是分分鐘的事。

效果:經過 useIsMounted 拿到 isMounted 狀態。

const isMounted = useIsMounted();

實現:看到這裏的話,應該已經很熟悉這個套路了,useEffect 第一次調用時賦值爲 true,組件銷燬時返回 false,注意這裏能夠加第二個參數爲空數組來優化性能。

const [isMount, setIsMount] = useState(false);
useEffect(() => {
if (!isMount) {
setIsMount(true);
}
return () => setIsMount(false);
}, []);
return isMount;

在線 Demo

存數據

上一篇提到過 React Hooks 內置的 useReducer 能夠模擬 Redux 的 reducer 行爲,那惟一須要補充的就是將數據持久化。咱們考慮最小實現,也就是全局 Store + Provider 部分。

全局 Store

效果:經過 createStore 建立一個全局 Store,再經過 StoreProvider 將 store 注入到子組件的 context 中,最終經過兩個 Hooks 進行獲取與操做:useStore 與 useAction

const store = createStore({
user: {
name: "小明",
setName: (state, payload) => {
state.name = payload;
}
}
});

const App = () => (
<StoreProvider store={store}>
<YourApp />
</StoreProvider>
);

function YourApp() {
const userName = useStore(state => state.user.name);
const setName = userAction(dispatch => dispatch.user.setName);
}

實現:這個例子的實現能夠單獨拎出一篇文章了,因此筆者從存數據的角度剖析一下 StoreProvider的實現。

對,Hooks 並不解決 Provider 的問題,因此全局狀態必須有 Provider,但這個 Provider 能夠利用 React 內置的 createContext 簡單搞定:

const StoreContext = createContext();

const StoreProvider = ({ children, store }) => (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);

剩下就是 useStore 怎麼取到持久化 Store 的問題了,這裏利用 useContext 和剛纔建立的 Context 對象:

const store = useContext(StoreContext);
return store;

更多源碼能夠參考 easy-peasy,這個庫基於 redux 編寫,提供了一套 Hooks API。

封裝原有庫

是否是 React Hooks 出現後,全部的庫都要重寫一次?固然不是,咱們看看其餘庫如何作改造。

RenderProps to Hooks

這裏拿 react-powerplug 舉例。

好比有一個 renderProps 庫,但願改形成 Hooks 的用法:

import { Toggle } from 'react-powerplug'

function App() {
return (
<Toggle initial={true}>
{({ on, toggle }) => (
<Checkbox checked={on} onChange={toggle} />
)}
</Toggle>
)
}
↓ ↓ ↓ ↓ ↓ ↓
import { useToggle } from 'react-powerhooks'

function App() {
const [on, toggle] = useToggle()
return <Checkbox checked={on} onChange={toggle} />
}

效果:假如我是 react-powerplug 的維護者,怎麼樣最小成本支持 React Hook? 說實話這個沒辦法一步作到,但能夠經過兩步實現。

export function Toggle() {
// 這是 Toggle 的源碼
// balabalabala..
}

const App = wrap(() => {
// 第一步:包 wrap
const [on, toggle] = useRenderProps(Toggle); // 第二步:包 useRenderProps
});

實現:首先解釋一下爲何要包兩層,首先 Hooks 必須遵循 React 的規範,咱們必須寫一個 useRenderProps 函數以符合 Hooks 的格式,**那問題是如何拿到 Toggle 給 render 的 on 與 toggle?**正常方式應該拿不到,因此退而求其次,將 useRenderProps 拿到的 Toggle 傳給 wrap,讓 wrap 構造 RenderProps 執行環境拿到 on 與 toggle 後,調用 useRenderProps 內部的 setArgs 函數,讓 const [on, toggle] = useRenderProps(Toggle) 實現曲線救國。

const wrappers = []; // 全局存儲 wrappers

export const useRenderProps = (WrapperComponent, wrapperProps) => {
const [args, setArgs] = useState([]);
const ref = useRef({});
if (!ref.current.initialized) {
wrappers.push({
WrapperComponent,
wrapperProps,
setArgs
});
}
useEffect(() => {
ref.current.initialized = true;
}, []);
return args; // 經過下面 wrap 調用 setArgs 獲取值。
};

因爲 useRenderProps 會先於 wrap 執行,因此 wrappers 會先拿到 Toggle,wrap 執行時直接調用 wrappers.pop() 便可拿到 Toggle 對象。而後構造出 RenderProps 的執行環境便可:

export const wrap = FunctionComponent => props => {
const element = FunctionComponent(props);
const ref = useRef({ wrapper: wrappers.pop() }); // 拿到 useRenderProps 提供的 Toggle
const { WrapperComponent, wrapperProps } = ref.current.wrapper;
return createElement(WrapperComponent, wrapperProps, (...args) => {
// WrapperComponent => Toggle,這一步是在構造 RenderProps 執行環境
if (!ref.current.processed) {
ref.current.wrapper.setArgs(args); // 拿到 on、toggle 後,經過 setArgs 傳給上面的 args。
ref.current.processed = true;
} else {
ref.current.processed = false;
}
return element;
});
};

以上實現方案參考 react-hooks-render-props,有需求要能夠拿過來直接用,不過實現思路能夠參考,做者的腦洞挺大。

Hooks to RenderProps

好吧,若是但願 Hooks 支持 RenderProps,那必定是但願同時支持這兩套語法。

效果:一套代碼同時支持 Hooks 和 RenderProps。

實現:其實 Hooks 封裝爲 RenderProps 最方便,所以咱們使用 Hooks 寫核心的代碼,假設咱們寫一個最簡單的 Toggle

const useToggle = initialValue => {
const [on, setOn] = useState(initialValue);
return {
on,
toggle: () => setOn(!on)
};
};

在線 Demo

而後經過 render-props 這個庫能夠輕鬆封裝出 RenderProps 組件:

const Toggle = ({ initialValue, children, render = children }) =>
renderProps(render, useToggle(initialValue));

在線 Demo

其實 renderProps 這個組件的第二個參數,在 Class 形式 React 組件時,接收的是 this.state,如今咱們改爲 useToggle 返回的對象,也能夠理解爲 state,利用 Hooks 機制驅動 Toggle 組件 rerender,從而讓子組件 rerender。

封裝本來對 setState 加強的庫

Hooks 也特別適合封裝本來就做用於 setState 的庫,好比 immer。

useState 雖然不是 setState,但卻能夠理解爲控制高階組件的 setState,咱們徹底能夠封裝一個自定義的 useState,而後內置對 setState 的優化。

好比 immer 的語法是經過 produce 包裝,將 mutable 代碼經過 Proxy 代理爲 immutable:

const nextState = produce(baseState, draftState => {
draftState.push({ todo: "Tweet about it" });
draftState[1].done = true;
});

那這個 produce 就能夠經過封裝一個 useImmer 來隱藏掉:

function useImmer(initialValue) {
const [val, updateValue] = React.useState(initialValue);
return [
val,
updater => {
updateValue(produce(updater));
}
];
}

使用方式:

const [value, setValue] = useImmer({ a: 1 });

value(obj => (obj.a = 2)); // immutable

總結

把 React Hooks 看成更便捷的 RenderProps 去用吧,雖然寫法看上去是內部維護了一個狀態,但其實等價於注入、Connect、HOC、或者 renderProps,那麼如此一來,使用 renderProps 的門檻會大大下降,由於 Hooks 用起來實在是太方便了,咱們能夠抽象大量 Custom Hooks,讓代碼更加 FP,同時也不會增長嵌套層級。

相關文章
相關標籤/搜索