摘要:可以看到,這樣不僅沒(méi)有占用組件自己的,也不需要手寫(xiě)回調(diào)函數(shù)進(jìn)行處理,這些處理都?jí)嚎s成了一行。效果通過(guò)拿到周期才執(zhí)行的回調(diào)函數(shù)。實(shí)現(xiàn)等價(jià)于的回調(diào)僅執(zhí)行一次時(shí),因此直接把回調(diào)函數(shù)拋出來(lái)即可。
1 引言
上周的 精讀《React Hooks》 已經(jīng)實(shí)現(xiàn)了對(duì) React Hooks 的基本認(rèn)知,也許你也看了 React Hooks 基本實(shí)現(xiàn)剖析(就是數(shù)組),但理解實(shí)現(xiàn)原理就可以用好了嗎?學(xué)的是知識(shí),而用的是技能,看別人的用法就像刷抖音一樣(哇,飯還可以這樣吃?),你總會(huì)有新的收獲。
這篇文章將這些知識(shí)實(shí)踐起來(lái),看看廣大程序勞動(dòng)人民是如何發(fā)掘 React Hooks 的潛力的(造什么輪子)。
首先,站在使用角度,要理解 React Hooks 的特點(diǎn)是 “非常方便的 Connect 一切”,所以無(wú)論是數(shù)據(jù)流、Network,或者是定時(shí)器都可以監(jiān)聽(tīng),有一點(diǎn) RXJS 的意味,也就是你可以利用 React Hooks,將 React 組件打造成:任何事物的變化都是輸入源,當(dāng)這些源變化時(shí)會(huì)重新觸發(fā) React 組件的 render,你只需要挑選組件綁定哪些數(shù)據(jù)源(use 哪些 Hooks),然后只管寫(xiě) render 函數(shù)就行了!
2 精讀參考了部分 React Hooks 組件后,筆者按照功能進(jìn)行了一些分類(lèi)。
由于 React Hooks 并不是非常復(fù)雜,所以就不按照技術(shù)實(shí)現(xiàn)方式去分類(lèi)了,畢竟技術(shù)總有一天會(huì)熟練,而且按照功能分類(lèi)才有持久的參考價(jià)值。DOM 副作用修改 / 監(jiān)聽(tīng)
做一個(gè)網(wǎng)頁(yè),總有一些看上去和組件關(guān)系不大的麻煩事,比如修改頁(yè)面標(biāo)題(切換頁(yè)面記得改成默認(rèn)標(biāo)題)、監(jiān)聽(tīng)頁(yè)面大小變化(組件銷(xiāo)毀記得取消監(jiān)聽(tīng))、斷網(wǎng)時(shí)提示(一層層裝飾器要堆成小山了)。而 React Hooks 特別擅長(zhǎng)做這些事,造這種輪子,大小皆宜。
由于 React Hooks 降低了高階組件使用成本,那么一套生命周期才能完成的 “雜?!?將變得非常簡(jiǎn)單。
下面舉幾個(gè)例子:
修改頁(yè)面 title效果:在組件里調(diào)用 useDocumentTitle 函數(shù)即可設(shè)置頁(yè)面標(biāo)題,且切換頁(yè)面時(shí),頁(yè)面標(biāo)題重置為默認(rèn)標(biāo)題 “前端精讀”。
useDocumentTitle("個(gè)人中心");
實(shí)現(xiàn):直接用 document.title 賦值,不能再簡(jiǎn)單。在銷(xiāo)毀時(shí)再次給一個(gè)默認(rèn)標(biāo)題即可,這個(gè)簡(jiǎn)單的函數(shù)可以抽象在項(xiàng)目工具函數(shù)里,每個(gè)頁(yè)面組件都需要調(diào)用。
function useDocumentTitle(title) { useEffect( () => { document.title = title; return () => (document.title = "前端精讀"); }, [title] ); }
在線(xiàn) Demo
監(jiān)聽(tīng)頁(yè)面大小變化,網(wǎng)絡(luò)是否斷開(kāi)效果:在組件調(diào)用 useWindowSize 時(shí),可以拿到頁(yè)面大小,并且在瀏覽器縮放時(shí)自動(dòng)觸發(fā)組件更新。
const windowSize = useWindowSize(); return頁(yè)面高度:{windowSize.innerWidth};
實(shí)現(xiàn):和標(biāo)題思路基本一致,這次從 window.innerHeight 等 API 直接拿到頁(yè)面寬高即可,注意此時(shí)可以用 window.addEventListener("resize") 監(jiān)聽(tīng)頁(yè)面大小變化,此時(shí)調(diào)用 setValue 將會(huì)觸發(fā)調(diào)用自身的 UI 組件 rerender,就是這么簡(jiǎn)單!
最后注意在銷(xiāo)毀時(shí),removeEventListener 注銷(xiāo)監(jiān)聽(tīng)。
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; }
在線(xiàn) Demo
動(dòng)態(tài)注入 css效果:在頁(yè)面注入一段 class,并且當(dāng)組件銷(xiāo)毀時(shí),移除這個(gè) class。
const className = useCss({ color: "red" }); returnText.;
實(shí)現(xiàn):可以看到,Hooks 方便的地方是在組件銷(xiāo)毀時(shí)移除副作用,所以我們可以安心的利用 Hooks 做一些副作用。注入 css 自然不必說(shuō)了,而銷(xiāo)毀 css 只要找到注入的那段引用進(jìn)行銷(xiāo)毀即可,具體可以看這個(gè) 代碼片段。
DOM 副作用修改 / 監(jiān)聽(tīng)場(chǎng)景有一些現(xiàn)成的庫(kù)了,從名字上就能看出來(lái)用法:document-visibility、network-status、online-status、window-scroll-position、window-size、document-title。組件輔助
Hooks 還可以增強(qiáng)組件能力,比如拿到并監(jiān)聽(tīng)組件運(yùn)行時(shí)寬高等。
獲取組件寬高效果:通過(guò)調(diào)用 useComponentSize 拿到某個(gè)組件 ref 實(shí)例的寬高,并且在寬高變化時(shí),rerender 并拿到最新的寬高。
const ref = useRef(null); let componentSize = useComponentSize(ref); return ( <> {componentSize.width} > );
實(shí)現(xiàn):和 DOM 監(jiān)聽(tīng)類(lèi)似,這次換成了利用 ResizeObserver 對(duì)組件 ref 進(jìn)行監(jiān)聽(tīng),同時(shí)在組件銷(xiāo)毀時(shí),銷(xiāo)毀監(jiān)聽(tīng)。
其本質(zhì)還是監(jiān)聽(tīng)一些副作用,但通過(guò) ref 的傳遞,我們可以對(duì)組件粒度進(jìn)行監(jiān)聽(tīng)和操作了。
useLayoutEffect(() => { handleResize(); let resizeObserver = new ResizeObserver(() => handleResize()); resizeObserver.observe(ref.current); return () => { resizeObserver.disconnect(ref.current); resizeObserver = null; }; }, []);
在線(xiàn) Demo,對(duì)應(yīng)組件 component-size。
拿到組件 onChange 拋出的值效果:通過(guò) useInputValue() 拿到 Input 框當(dāng)前用戶(hù)輸入的值,而不是手動(dòng)監(jiān)聽(tīng) onChange 再騰一個(gè) otherInputValue 和一個(gè)回調(diào)函數(shù)把這一堆邏輯寫(xiě)在無(wú)關(guān)的地方。
let name = useInputValue("Jamie"); // name = { value: "Jamie", onChange: [Function] } return ;
可以看到,這樣不僅沒(méi)有占用組件自己的 state,也不需要手寫(xiě) onChange 回調(diào)函數(shù)進(jìn)行處理,這些處理都?jí)嚎s成了一行 use hook。
實(shí)現(xiàn):讀到這里應(yīng)該大致可以猜到了,利用 useState 存儲(chǔ)組件的值,并拋出 value 與 onChange,監(jiān)聽(tīng) onChange 并通過(guò) setValue 修改 value, 就可以在每次 onChange 時(shí)觸發(fā)調(diào)用組件的 rerender 了。
function useInputValue(initialValue) { let [value, setValue] = useState(initialValue); let onChange = useCallback(function(event) { setValue(event.currentTarget.value); }, []); return { value, onChange }; }
這里要注意的是,我們對(duì)組件增強(qiáng)時(shí),組件的回調(diào)一般不需要銷(xiāo)毀監(jiān)聽(tīng),而且僅需監(jiān)聽(tīng)一次,這與 DOM 監(jiān)聽(tīng)不同,因此大部分場(chǎng)景,我們需要利用 useCallback 包裹,并傳一個(gè)空數(shù)組,來(lái)保證永遠(yuǎn)只監(jiān)聽(tīng)一次,而且不需要在組件銷(xiāo)毀時(shí)注銷(xiāo)這個(gè) callback。
在線(xiàn) Demo,對(duì)應(yīng)組件 input-value。
做動(dòng)畫(huà)利用 React Hooks 做動(dòng)畫(huà),一般是拿到一些具有彈性變化的值,我們可以將值賦給進(jìn)度條之類(lèi)的組件,這樣其進(jìn)度變化就符合某種動(dòng)畫(huà)曲線(xiàn)。
在某個(gè)時(shí)間段內(nèi)獲取 0-1 之間的值這個(gè)是動(dòng)畫(huà)最基本的概念,某個(gè)時(shí)間內(nèi)拿到一個(gè)線(xiàn)性增長(zhǎng)的值。
效果:通過(guò) useRaf(t) 拿到 t 毫秒內(nèi)不斷刷新的 0-1 之間的數(shù)字,期間組件會(huì)不斷刷新,但刷新頻率由 requestAnimationFrame 控制(不會(huì)卡頓 UI)。
const value = useRaf(1000);
實(shí)現(xiàn):寫(xiě)起來(lái)比較冗長(zhǎng),這里簡(jiǎn)單描述一下。利用 requestAnimationFrame 在給定時(shí)間內(nèi)給出 0-1 之間的值,那每次刷新時(shí),只要判斷當(dāng)前刷新的時(shí)間點(diǎn)占總時(shí)間的比例是多少,然后做分母,分子是 1 即可。
在線(xiàn) Demo,對(duì)應(yīng)組件 use-raf。
彈性動(dòng)畫(huà)效果:通過(guò) useSpring 拿到動(dòng)畫(huà)值,組件以固定頻率刷新,而這個(gè)動(dòng)畫(huà)值以彈性函數(shù)進(jìn)行增減。
實(shí)際調(diào)用方式一般是,先通過(guò) useState 拿到一個(gè)值,再通過(guò)動(dòng)畫(huà)函數(shù)包住這個(gè)值,這樣組件就會(huì)從原本的刷新一次,變成刷新 N 次,拿到的值也隨著動(dòng)畫(huà)函數(shù)的規(guī)則變化,最后這個(gè)值會(huì)穩(wěn)定到最終的輸入值(如例子中的 50)。
const [target, setTarget] = useState(50); const value = useSpring(target); returnsetTarget(100)}>{value};
實(shí)現(xiàn):為了實(shí)現(xiàn)動(dòng)畫(huà)效果,需要依賴(lài) rebound 庫(kù),它可以實(shí)現(xiàn)將一個(gè)目標(biāo)值拆解為符合彈性動(dòng)畫(huà)函數(shù)過(guò)程的功能,那我們需要利用 React Hooks 做的就是在第一次接收到目標(biāo)值是,調(diào)用 spring.setEndValue 來(lái)觸發(fā)動(dòng)畫(huà)事件,并在 useEffect 里做一次性監(jiān)聽(tīng),再值變時(shí)重新 setValue 即可。
最神奇的 setTarget 聯(lián)動(dòng) useSpring 重新計(jì)算彈性動(dòng)畫(huà)部分,是通過(guò) useEffect 第二個(gè)參數(shù)實(shí)現(xiàn)的:
useEffect( () => { if (spring) { spring.setEndValue(targetValue); } }, [targetValue] );
也就是當(dāng)目標(biāo)值變化后,才會(huì)進(jìn)行新的一輪 rerender,所以 useSpring 并不需要監(jiān)聽(tīng)調(diào)用處的 setTarget,它只需要監(jiān)聽(tīng) target 的變化即可,而巧妙利用 useEffect 的第二個(gè)參數(shù)可以事半功倍。
在線(xiàn) Demo
Tween 動(dòng)畫(huà)明白了彈性動(dòng)畫(huà)原理,Tween 動(dòng)畫(huà)就更簡(jiǎn)單了。
效果:通過(guò) useTween 拿到一個(gè)從 0 變化到 1 的值,這個(gè)值的動(dòng)畫(huà)曲線(xiàn)是 tween。可以看到,由于取值范圍是固定的,所以我們不需要給初始值了。
const value = useTween();
實(shí)現(xiàn):通過(guò) useRaf 拿到一個(gè)線(xiàn)性增長(zhǎng)的值(區(qū)間也是 0 ~ 1),再通過(guò) easing 庫(kù)將其映射到 0 ~ 1 到值即可。這里用到了 hook 調(diào)用 hook 的聯(lián)動(dòng)(通過(guò) useRaf 驅(qū)動(dòng) useTween),還可以在其他地方舉一反三。
const fn: Easing = easing[easingName]; const t = useRaf(ms, delay); return fn(t);發(fā)請(qǐng)求
利用 Hooks,可以將任意請(qǐng)求 Promise 封裝為帶有標(biāo)準(zhǔn)狀態(tài)的對(duì)象:loading、error、result。
通用 Http 封裝效果:通過(guò) useAsync 將一個(gè) Promise 拆解為 loading、error、result 三個(gè)對(duì)象。
const { loading, error, result } = useAsync(fetchUser, [id]);
實(shí)現(xiàn):在 Promise 的初期設(shè)置 loading,結(jié)束后設(shè)置 result,如果出錯(cuò)則設(shè)置 error,這里可以將請(qǐng)求對(duì)象包裝成 useAsyncState 來(lái)處理,這里就不放出來(lái)了。
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,這個(gè)功能建議僅了解原理,具體實(shí)現(xiàn)因?yàn)橛幸恍┻吔缜闆r需要考慮,比如組件 isMounted 后才能相應(yīng)請(qǐng)求結(jié)果。
Request Service業(yè)務(wù)層一般會(huì)抽象一個(gè) request service 做統(tǒng)一取數(shù)的抽象(比如統(tǒng)一 url,或者可以統(tǒng)一換 socket 實(shí)現(xiàn)等等)。假如以前比較 low 的做法是:
async componentDidMount() { // setState: 改 isLoading state try { const data = await fetchUser() // setState: 改 isLoading、error、data } catch (error) { // setState: 改 isLoading、error } }
后來(lái)把請(qǐng)求放在 redux 里,通過(guò) connect 注入的方式會(huì)稍微有些改觀:
@Connect(...) class App extends React.PureComponent { public componentDidMount() { this.props.fetchUser() } public render() { // this.props.userData.isLoading | error | data } }
最后會(huì)發(fā)現(xiàn)還是 Hooks 簡(jiǎn)潔明了:
function App() { const { isLoading, error, data } = useFetchUser(); }
而 useFetchUser 利用上面封裝的 useAsync 可以很容易編寫(xiě):
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 版,那用起來(lái)會(huì)方便許多:
function App() { const { getFieldDecorator } = useAntdForm(); return (); }
不過(guò)雖然如此,getFieldDecorator 還是基于 RenderProps 思路的,徹底的 Hooks 思路是利用之前說(shuō)的 組件輔助方式,提供一個(gè)組件方法集,用解構(gòu)方式傳給組件。
Hooks 思維的表單組件效果:通過(guò) useFormState 拿到表單值,并且提供一系列 組件輔助 方法控制組件狀態(tài)。
const [formState, { text, password }] = useFormState(); return ();
上面可以通過(guò) formState 隨時(shí)拿到表單值,和一些校驗(yàn)信息,通過(guò) password("pwd") 傳給 input 組件,讓這個(gè)組件達(dá)到受控狀態(tài),且輸入類(lèi)型是 password 類(lèi)型,表單 key 是 pwd。而且可以看到使用的 form 是原生標(biāo)簽,這種表單增強(qiáng)是相當(dāng)解耦的。
實(shí)現(xiàn):仔細(xì)觀察一下結(jié)構(gòu),不難發(fā)現(xiàn),我們只要結(jié)合 組件輔助 小節(jié)說(shuō)的 “拿到組件 onChange 拋出的值” 一節(jié)的思路,就能輕松理解 text、password 是如何作用于 input 組件,并拿到其輸入狀態(tài)。
往簡(jiǎn)單的來(lái)說(shuō),只要把這些狀態(tài) Merge 起來(lái),通過(guò) useReducer 聚合到 formState 就可以實(shí)現(xiàn)了。
為了簡(jiǎn)化,我們只考慮對(duì) input 的增強(qiáng),源碼僅需 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 }); // 修改對(duì)應(yīng) Key 的值 } }; return inputProps; }; const inputPropsCreators = ["text", "password"].reduce( (methods, type) => ({ ...methods, [type]: createPropsGetter(type) }), {} ); return [ { values: state }, // formState inputPropsCreators ]; }
上面 30 行代碼實(shí)現(xiàn)了對(duì) input 標(biāo)簽類(lèi)型的設(shè)置,監(jiān)聽(tīng) value onChange,最終聚合到大的 values 作為 formState 返回。讀到這里應(yīng)該發(fā)現(xiàn)對(duì) React Hooks 的應(yīng)用都是萬(wàn)變不離其宗的,特別是對(duì)組件信息的獲取,通過(guò)解構(gòu)方式來(lái)做,Hooks 內(nèi)部再做一下聚合,就完成表單組件基本功能了。
實(shí)際上一個(gè)完整的輪子還需要考慮 checkbox radio 的兼容,以及校驗(yàn)問(wèn)題,這些思路大同小異,具體源碼可以看 react-use-form-state。
模擬生命周期有的時(shí)候 React15 的 API 還是挺有用的,利用 React Hooks 幾乎可以模擬出全套。
componentDidMount效果:通過(guò) useMount 拿到 mount 周期才執(zhí)行的回調(diào)函數(shù)。
useMount(() => { // quite similar to `componentDidMount` });
實(shí)現(xiàn):componentDidMount 等價(jià)于 useEffect 的回調(diào)(僅執(zhí)行一次時(shí)),因此直接把回調(diào)函數(shù)拋出來(lái)即可。
useEffect(() => void fn(), []);componentWillUnmount
效果:通過(guò) useUnmount 拿到 unmount 周期才執(zhí)行的回調(diào)函數(shù)。
useUnmount(() => { // quite similar to `componentWillUnmount` });
實(shí)現(xiàn):componentWillUnmount 等價(jià)于 useEffect 的回調(diào)函數(shù)返回值(僅執(zhí)行一次時(shí)),因此直接把回調(diào)函數(shù)返回值拋出來(lái)即可。
useEffect(() => fn, []);componentDidUpdate
效果:通過(guò) useUpdate 拿到 didUpdate 周期才執(zhí)行的回調(diào)函數(shù)。
useUpdate(() => { // quite similar to `componentDidUpdate` });
實(shí)現(xiàn):componentDidUpdate 等價(jià)于 useMount 的邏輯每次執(zhí)行,除了初始化第一次。因此采用 mouting flag(判斷初始狀態(tài))+ 不加限制參數(shù)確保每次 rerender 都會(huì)執(zhí)行即可。
const mounting = useRef(true); useEffect(() => { if (mounting.current) { mounting.current = false; } else { fn(); } });Force Update
效果:這個(gè)最有意思了,我希望拿到一個(gè)函數(shù) update,每次調(diào)用就強(qiáng)制刷新當(dāng)前組件。
const update = useUpdate();
實(shí)現(xiàn):我們知道 useState 下標(biāo)為 1 的項(xiàng)是用來(lái)更新數(shù)據(jù)的,而且就算數(shù)據(jù)沒(méi)有變化,調(diào)用了也會(huì)刷新組件,所以我們可以把返回一個(gè)沒(méi)有修改數(shù)值的 setValue,這樣它的功能就僅剩下刷新組件了。
const useUpdate = () => useState(0)[1];
對(duì)于 getSnapshotBeforeUpdate, getDerivedStateFromError, componentDidCatch 目前 Hooks 是無(wú)法模擬的。isMounted
很久以前 React 是提供過(guò)這個(gè) API 的,后來(lái)移除了,原因是可以通過(guò) componentWillMount 和 componentWillUnmount 推導(dǎo)。自從有了 React Hooks,支持 isMount 簡(jiǎn)直是分分鐘的事。
效果:通過(guò) useIsMounted 拿到 isMounted 狀態(tài)。
const isMounted = useIsMounted();
實(shí)現(xiàn):看到這里的話(huà),應(yīng)該已經(jīng)很熟悉這個(gè)套路了,useEffect 第一次調(diào)用時(shí)賦值為 true,組件銷(xiāo)毀時(shí)返回 false,注意這里可以加第二個(gè)參數(shù)為空數(shù)組來(lái)優(yōu)化性能。
const [isMount, setIsMount] = useState(false); useEffect(() => { if (!isMount) { setIsMount(true); } return () => setIsMount(false); }, []); return isMount;
在線(xiàn) Demo
存數(shù)據(jù)上一篇提到過(guò) React Hooks 內(nèi)置的 useReducer 可以模擬 Redux 的 reducer 行為,那唯一需要補(bǔ)充的就是將數(shù)據(jù)持久化。我們考慮最小實(shí)現(xiàn),也就是全局 Store + Provider 部分。
全局 Store效果:通過(guò) createStore 創(chuàng)建一個(gè)全局 Store,再通過(guò) StoreProvider 將 store 注入到子組件的 context 中,最終通過(guò)兩個(gè) Hooks 進(jìn)行獲取與操作:useStore 與 useAction:
const store = createStore({ user: { name: "小明", setName: (state, payload) => { state.name = payload; } } }); const App = () => (); function YourApp() { const userName = useStore(state => state.user.name); const setName = userAction(dispatch => dispatch.user.setName); }
實(shí)現(xiàn):這個(gè)例子的實(shí)現(xiàn)可以多帶帶拎出一篇文章了,所以筆者從存數(shù)據(jù)的角度剖析一下 StoreProvider 的實(shí)現(xiàn)。
對(duì),Hooks 并不解決 Provider 的問(wèn)題,所以全局狀態(tài)必須有 Provider,但這個(gè) Provider 可以利用 React 內(nèi)置的 createContext 簡(jiǎn)單搞定:
const StoreContext = createContext(); const StoreProvider = ({ children, store }) => ({children} );
剩下就是 useStore 怎么取到持久化 Store 的問(wèn)題了,這里利用 useContext 和剛才創(chuàng)建的 Context 對(duì)象:
const store = useContext(StoreContext); return store;
更多源碼可以參考 easy-peasy,這個(gè)庫(kù)基于 redux 編寫(xiě),提供了一套 Hooks API。
封裝原有庫(kù)是不是 React Hooks 出現(xiàn)后,所有的庫(kù)都要重寫(xiě)一次?當(dāng)然不是,我們看看其他庫(kù)如何做改造。
RenderProps to Hooks這里拿 react-powerplug 舉例。
比如有一個(gè) renderProps 庫(kù),希望改造成 Hooks 的用法:
import { Toggle } from "react-powerplug" function App() { return ({({ on, toggle }) => ( ) } ↓ ↓ ↓ ↓ ↓ ↓ import { useToggle } from "react-powerhooks" function App() { const [on, toggle] = useToggle() return)} }
效果:假如我是 react-powerplug 的維護(hù)者,怎么樣最小成本支持 React Hook? 說(shuō)實(shí)話(huà)這個(gè)沒(méi)辦法一步做到,但可以通過(guò)兩步實(shí)現(xiàn)。
export function Toggle() { // 這是 Toggle 的源碼 // balabalabala.. } const App = wrap(() => { // 第一步:包 wrap const [on, toggle] = useRenderProps(Toggle); // 第二步:包 useRenderProps });
實(shí)現(xiàn):首先解釋一下為什么要包兩層,首先 Hooks 必須遵循 React 的規(guī)范,我們必須寫(xiě)一個(gè) useRenderProps 函數(shù)以符合 Hooks 的格式,那問(wèn)題是如何拿到 Toggle 給 render 的 on 與 toggle?正常方式應(yīng)該拿不到,所以退而求其次,將 useRenderProps 拿到的 Toggle 傳給 wrap,讓 wrap 構(gòu)造 RenderProps 執(zhí)行環(huán)境拿到 on 與 toggle 后,調(diào)用 useRenderProps 內(nèi)部的 setArgs 函數(shù),讓 const [on, toggle] = useRenderProps(Toggle) 實(shí)現(xiàn)曲線(xiàn)救國(guó)。
const wrappers = []; // 全局存儲(chǔ) 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; // 通過(guò)下面 wrap 調(diào)用 setArgs 獲取值。 };
由于 useRenderProps 會(huì)先于 wrap 執(zhí)行,所以 wrappers 會(huì)先拿到 Toggle,wrap 執(zhí)行時(shí)直接調(diào)用 wrappers.pop() 即可拿到 Toggle 對(duì)象。然后構(gòu)造出 RenderProps 的執(zhí)行環(huán)境即可:
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,這一步是在構(gòu)造 RenderProps 執(zhí)行環(huán)境 if (!ref.current.processed) { ref.current.wrapper.setArgs(args); // 拿到 on、toggle 后,通過(guò) setArgs 傳給上面的 args。 ref.current.processed = true; } else { ref.current.processed = false; } return element; }); };
以上實(shí)現(xiàn)方案參考 react-hooks-render-props,有需求要可以拿過(guò)來(lái)直接用,不過(guò)實(shí)現(xiàn)思路可以參考,作者的腦洞挺大。
Hooks to RenderProps好吧,如果希望 Hooks 支持 RenderProps,那一定是希望同時(shí)支持這兩套語(yǔ)法。
效果:一套代碼同時(shí)支持 Hooks 和 RenderProps。
實(shí)現(xiàn):其實(shí) Hooks 封裝為 RenderProps 最方便,因此我們使用 Hooks 寫(xiě)核心的代碼,假設(shè)我們寫(xiě)一個(gè)最簡(jiǎn)單的 Toggle:
const useToggle = initialValue => { const [on, setOn] = useState(initialValue); return { on, toggle: () => setOn(!on) }; };
在線(xiàn) Demo
然后通過(guò) render-props 這個(gè)庫(kù)可以輕松封裝出 RenderProps 組件:
const Toggle = ({ initialValue, children, render = children }) => renderProps(render, useToggle(initialValue));
在線(xiàn) Demo
其實(shí) renderProps 這個(gè)組件的第二個(gè)參數(shù),在 Class 形式 React 組件時(shí),接收的是 this.state,現(xiàn)在我們改成 useToggle 返回的對(duì)象,也可以理解為 state,利用 Hooks 機(jī)制驅(qū)動(dòng) Toggle 組件 rerender,從而讓子組件 rerender。
封裝原本對(duì) setState 增強(qiáng)的庫(kù)Hooks 也特別適合封裝原本就作用于 setState 的庫(kù),比如 immer。
useState 雖然不是 setState,但卻可以理解為控制高階組件的 setState,我們完全可以封裝一個(gè)自定義的 useState,然后內(nèi)置對(duì) setState 的優(yōu)化。
比如 immer 的語(yǔ)法是通過(guò) produce 包裝,將 mutable 代碼通過(guò) Proxy 代理為 immutable:
const nextState = produce(baseState, draftState => { draftState.push({ todo: "Tweet about it" }); draftState[1].done = true; });
那這個(gè) produce 就可以通過(guò)封裝一個(gè) useImmer 來(lái)隱藏掉:
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)); // immutable3 總結(jié)
本文列出了 React Hooks 的以下幾種使用方式以及實(shí)現(xiàn)思路:
DOM 副作用修改 / 監(jiān)聽(tīng)。
組件輔助。
做動(dòng)畫(huà)。
發(fā)請(qǐng)求。
填表單。
模擬生命周期。
存數(shù)據(jù)。
封裝原有庫(kù)。
歡迎大家的持續(xù)補(bǔ)充。
4 更多討論討論地址是:精讀《怎么用 React Hooks 造輪子》 · Issue #112 · dt-fe/weekly
如果你想?yún)⑴c討論,請(qǐng)點(diǎn)擊這里,每周都有新的主題,周末或周一發(fā)布。前端精讀 - 幫你篩選靠譜的內(nèi)容。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/108696.html
摘要:未來(lái)可能成為官方之一。討論地址是精讀組件如果你想?yún)⑴c討論,請(qǐng)點(diǎn)擊這里,每周都有新的主題,周末或周一發(fā)布。前端精讀幫你篩選靠譜的內(nèi)容。 1. 引言 為什么要了解 Function 寫(xiě)法的組件呢?因?yàn)樗谧兊迷絹?lái)越重要。 那么 React 中 Function Component 與 Class Component 有何不同? how-are-function-components-di...
摘要:拿到的都是而不是原始值,且這個(gè)值會(huì)動(dòng)態(tài)變化。精讀對(duì)于的與,筆者做一些對(duì)比。因此采取了作為優(yōu)化方案只有當(dāng)?shù)诙€(gè)依賴(lài)參數(shù)變化時(shí)才返回新引用。不需要使用等進(jìn)行性能優(yōu)化,所有性能優(yōu)化都是自動(dòng)的。前端精讀幫你篩選靠譜的內(nèi)容。 1. 引言 Vue 3.0 的發(fā)布引起了軒然大波,讓我們解讀下它的 function api RFC 詳細(xì)了解一下 Vue 團(tuán)隊(duì)是怎么想的吧! 首先官方回答了幾個(gè)最受關(guān)注的...
摘要:今天我們就來(lái)解讀一下的源碼。比較有意思,將定時(shí)器以方式提供出來(lái),并且提供了方法。實(shí)現(xiàn)方式是,在組件內(nèi)部維護(hù)一個(gè)定時(shí)器,實(shí)現(xiàn)了組件更新銷(xiāo)毀時(shí)的計(jì)時(shí)器更新銷(xiāo)毀操作,可以認(rèn)為這種定時(shí)器的生命周期綁定了組件的生命周期,不用擔(dān)心銷(xiāo)毀和更新的問(wèn)題。 1. 引言 React PowerPlug 是利用 render props 進(jìn)行更好狀態(tài)管理的工具庫(kù)。 React 項(xiàng)目中,一般一個(gè)文件就是一個(gè)類(lèi),...
摘要:引言于發(fā)布版本,時(shí)至今日已更新到,且引入了大量的令人振奮的新特性,本文章將帶領(lǐng)大家根據(jù)更新的時(shí)間脈絡(luò)了解的新特性。其作用是根據(jù)傳遞的來(lái)更新。新增等指針事件。 1 引言 于 2017.09.26 Facebook 發(fā)布 React v16.0 版本,時(shí)至今日已更新到 React v16.6,且引入了大量的令人振奮的新特性,本文章將帶領(lǐng)大家根據(jù) React 更新的時(shí)間脈絡(luò)了解 React1...
摘要:需要說(shuō)明是的,這里說(shuō)的專(zhuān)家不再關(guān)心細(xì)節(jié),不代表成為專(zhuān)家后學(xué)不會(huì)細(xì)節(jié),也不代表專(zhuān)家不了解細(xì)節(jié)。本文將從三個(gè)點(diǎn)去解釋?zhuān)瑸槭裁磳?zhuān)家看上去越來(lái)越原理技術(shù)細(xì)節(jié)。試想一個(gè)不能理解業(yè)務(wù)要做什么的人,即便懂得再多技術(shù)細(xì)節(jié),對(duì)業(yè)務(wù)也是沒(méi)有價(jià)值的。1. 引言 本周的精讀是有感而發(fā)。 筆者接觸前端已有八年,觀察了不少前端大牛的發(fā)展路徑,發(fā)現(xiàn)成功的人都具有相似的經(jīng)歷: 初期技術(shù)熱情極大 -> 大量標(biāo)志性技術(shù)項(xiàng)目 -...
閱讀 983·2023-04-26 01:34
閱讀 3430·2023-04-25 20:58
閱讀 3602·2021-11-08 13:22
閱讀 2168·2019-08-30 14:17
閱讀 2575·2019-08-29 15:27
閱讀 2737·2019-08-29 12:45
閱讀 3103·2019-08-29 12:26
閱讀 2871·2019-08-28 17:51