前言
千呼萬喚始出來,React Hooks終於在React 16.8版本中釋出穩定版了。最近逛github發現了一個很有意思的庫:react-hanger。
複習React Hooks
如果對Hooks還不怎麼了解的同學,建議去看一下官方文件:Introducing Hooks.
什麼是 Hooks?
我們都知道,在Hooks之前,開發react元件主要是class元件和function元件。function元件沒有state,所以也叫SFC(stateless functional component),簡單的將props對映成view;class元件有state,能夠處理更加複雜的邏輯。但是基於class的組建並不是完美的,總結起來就像Dan說的那樣,有三個主要的問題:
- 程式碼重用:在hooks出來之前,常見的程式碼重用方式是HOCs和render props,這兩種方式帶來的問題是:你需要解構自己的元件,非常的笨重,同時會帶來很深的元件巢狀
- 複雜的元件邏輯:在class元件中,有許多的lifecycle 函式,你需要在各個函式的裡面去做對應的事情。這種方式帶來的痛點是:邏輯分散在各處,開發者去維護這些程式碼會分散自己的精力,理解程式碼邏輯也很吃力
- class元件的困惑:對於初學者來說,需要理解class元件裡面的this是比較吃力的(這個理由有點勉強~),同時,基於class的元件難以優化(舉個不恰當的例子,看一下babel轉移出來的class程式碼量增長了多少)
為了解決上面的這三個問題,react hooks提案登場了,它有以下幾個特點:
- 無痛接入,不破壞現有的專案結構
- 完全向後相容,不包含任何不相容breaking changes
- 現在就能使用
Hooks 允許你在不編寫 class 的情況下使用狀態(state)和其他 React 特性。 你還可以構建自己的 Hooks, 跨元件共享可重用的有狀態邏輯。
現在React中內建的Hooks有:
當然了,授之以魚不如授之以漁,React官方也提供了教你如何封裝自己Hook的文件Building Your Own Hooks,有興趣的小夥伴可以去閱讀一下。
react-hanger初窺
大致的看了下react-hanger的原始碼之後發現,這個庫其實是對React Hooks API的適用性封裝。暴露一些更常用的Hooks節省大家造輪子的工作量。
React的核心開發者Dan看到這個庫也做了評價:
一個對Hooks的隱喻。你可以將你的state“掛起”在你的function component上,等你回來的時候,它就掛在那。
本文寫作時,react-hanger的Usage裡提供了6個API,從名字裡就可以看出這些Hook都是做什麼的(Hooks都以"use"開頭,這是一種約定),
import {
useInput,
useBoolean,
useNumber,
useArray,
useOnMount,
useOnUnmount
} from "react-hanger";
複製程式碼
使用起來也很簡單,比如useNumber
const App = () => {
const showCounter = useBoolean(true);
const counter = useNumber(0);
return (
<div>
<button onClick={counter.increase}> increase </button>
{showCounter.value && <span> {counter.value} </span>}
<button onClick={counter.decrease}> decrease </button>
</div>
);
};
複製程式碼
初步印象:大致與原始的basic hooks有點不同的是,useState返回一個陣列,分別是值
與操作
,而react-hanger提供的API貌似是將值
和一些操作
封裝到一個物件中,比如counter
就是一個{value: count, increase: setCount(count + 1), decrease: setCount(count - 1) }
的物件。
還有更多的操作方法可以看react-hanger的sandbox:https://codesandbox.io/s/44m70xm70
react-hanger原始碼淺析
其實翻看了react-hanger的原始碼之後會發現,react-hanger一共引用了四個React內建的Hook,
import { useCallback, useEffect, useRef, useState } from "react";
複製程式碼
然後返回一些“輪子”hooks,包括useNumber
、useArray
、useBoolean
等等。
這些輪子可以大致分為兩類:封裝Hook和拆分Hook。
封裝Hook
比如useStateful
、useNumber
、useArray
、useBoolean
都是對內建HookuseState
的封裝。
useStateful
export const useStateful = initial => {
const [value, setValue] = useState(initial);
return {
value,
setValue
};
};
複製程式碼
利用ES6的解構賦值,將useState
返回的陣列封裝成一個物件重新返回,方便呼叫。
useNumber
export const useNumber = (
initial,
{ upperLimit, lowerLimit, loop, step = 1 } = {}
) => {
const [value, setValue] = useState(initial);
return {
value,
setValue,
increase: useCallback(i => {
setValue(...);
}, []),
decrease: useCallback(d => {
setValue(...);
}, [])
};
};
複製程式碼
useNumber
接收一個initial number和一個配置項物件,在內部是通過對initial number進行useState Hook,返回一個物件,除了基本的value
和setValue
,還有兩個方法increase
和decrease
。這兩個方法都是用useCallback
對setValue
進行的進一步封裝。
而
useCallback
是一個比較重要的內建Hook,useCallback
的可以於快取了每次渲染時 inline callback 的例項,在第二個引數陣列內的值發生更改時才會更改。這樣可以配合上子元件的
shouldComponentUpdate
或者useMemo
起到減少不必要的渲染的作用。
而第二個引數為空陣列的意思就是告訴React不管引數如何都要記憶。
useArray & useBoolean & useInput
至於useArray
、useBoolean
、 useInput
這三個hook可以說和useNumber
大同小異,都是需要一個傳入的initial值,在hook內部通過useState
初始化,再返回一些常用的操作方法。
這裡的useInput
是針對於受控元件,所以不需要useRef
。
useSetState
export const useSetState = initialValue => {
const { value, setValue } = useStateful(initialValue);
return {
setState: useCallback(v => {
return setValue(oldValue => ({
...oldValue,
...(typeof v === "function" ? v(oldValue) : v)
}));
}, []),
state: value
};
};
複製程式碼
Unlike the
setState
method found in class components,useState
does not automatically merge update objects.與類元件中的setState方法不同,useState不會自動合併更新物件。
熟悉React Hook的同學看了程式碼就知道這個hook是封裝了什麼了,因為useState返回的類似於setCount
的方法不會自動合併更新物件。這個hook幫助大家可以獲得一個可以merge之前value的Hook型setState
。
拆分Hook
上述幾個算是封裝hook,那麼下面的幾個就可以算是拆分hook,對useEffect
更精細化的處理。
useOnMount & useOnUnmount
眾所周知,useEffect
是被用來處理一些原先放在class元件中生命週期函式的副作用,比如componentDidMount
、componentDidUpdate
、componentWillUnmount
,集合而成的一個Hook。
理論上,在每次渲染後都會觸發useEffect
的效果,但是如果我只想在didmount裡或者只想在willunmount裡做一下事情,該怎麼辦?
這時就用到了useEffect
的一個特點:第二個引數為效果依賴的值陣列,也就是說只有當陣列內的值變化才會觸發useEffect
,
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
複製程式碼
而如果第二個引數為一個空陣列的時候,則相當於告訴React你的效果不依賴於元件中的任何值,因此該效果只能在mount上執行並在unmount上清理,它不會在更新時執行。
export const useOnUnmount = onUnmount =>
useEffect(() => {
return () => onUnmount && onUnmount();
}, []);
export const useOnMount = onMount =>
useEffect(() => {
onMount && onMount();
}, []);
複製程式碼
所以useOnMount
的實現就非常簡單,在useEffect
內執行onMount函式且第二個引數是[]
,useOnUnmount
的實現則是返回onUnmount函式且第二個引數是[]
。
useLifecycleHooks
export const useLifecycleHooks = ({ onMount, onUnmount }) =>
useEffect(() => {
onMount && onMount();
return () => onUnmount && onUnmount();
}, []);
複製程式碼
useLifecycleHooks
則是對useOnUnmount
和useOnMount
的整合,在useEffect
的第二個引數為[]
的情況下,執行onMount和返回onUnmount。
useLogger
export const useLogger = (name, props) => {
useLifecycleHooks({
onMount: () => console.log(`${name} has mounted`),
onUnmount: () => console.log(`${name} has unmounted`)
});
useEffect(() => {
console.log("Props updated", props);
});
};
複製程式碼
useLogger
算是一個為hook commponent封裝的log外掛,通過在useLifecycleHooks
內傳入onMount和onUnmount列印日誌的函式,之後再通過原生的預設useEffect
不傳遞第二個引數來實現在更新過程中列印日誌。
usePrevious
export const usePrevious = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
複製程式碼
usePrevious則可以獲取之前的props或者state,來自於React的官方文件。
總結
其實react-hanger的原始碼都很簡潔,對原始碼感興趣的同學請看:https://github.com/kitze/react-hanger/blob/master/src/index.js,更多的是這個庫的實現大部分都是用了React Hook的思想,希望大家可以通過本文加深對React Hook的理解和認識。