React hooks 發佈於 React V16.8.0,想了解 react-hooks 基本概念的同窗能夠參考 官方文檔,想深刻了解 useEffect 的同窗能夠看一下 Dan Abramov 寫的 A complete guide to useEffect,對於 react-hooks 的 rules 有疑惑的同窗能夠參考 React hooks:not magic, just arrays 這一篇文章。
這篇文章的目的在於記錄我在學習和應用 react-hooks 過程當中的一些感悟,也但願能對你學習和應用 react-hooks 提供幫助。html
以前 React 對於狀態管理沒有提供很好的支持,因此咱們會依賴 redux 和 mobx 這些第三方的狀態管理庫。redux 很好地實踐了 immutable 和 pure function;mobx 則是 mutable 和 reactive 的表明。如今使用 React 內置 useReducer 和 useContext 這兩個 hook 可讓咱們實現 redux 風格的狀態管理;結合 useMemo 和 useEffect 能夠模擬 mobx 的 computed 和 reaction。(hooks 沒有使用 Proxy,因此跟 mobx 的代理響應是不同的)
下面咱們使用 hooks 實現一個原生的狀態管理庫。前端
跟 redux 同樣,咱們使用 React Context 實現 global store:node
// store.ts import { useContext, createContext, useReducer } from 'react' // User 和 Env 是 reducer 文件 import { default as User } from './User' import { default as Env } from './Env' // 建立一個全局 context // 這裏使用了 ReturnType 做爲 Context 的範型聲明 export const GlobalContext = createContext<ReturnType<typeof useGlobalStore>>( null ) // custom hook:封裝全部全局數據,用於 GlobalContext.Provider 的 value 屬性賦值 export function useGlobalStore() { const currentUser = useReducer(User.reducer, User.init()) const env = useReducer(Env.reducer, Env.init()) return { currentUser, env, } } // custom hook:實現了 GlobalContext.Consumer 的功能 export function useGlobal() { return useContext(GlobalContext) }
上面的代碼定義了 store 模塊做爲項目的全局狀態庫,導出了三個實體:react
下面是使用 store 模塊封裝的 Provider 組件:git
// GlobalProvider.tsx import React from 'react' import { GlobalContext, useGlobalStore } from './store' export function GlobalProvider({ children }) { const store = useGlobalStore() return ( <GlobalContext.Provider value={store}>{children}</GlobalContext.Provider> ) }
將 Provider 模塊引用到項目根組件:typescript
// App.tsx import React from 'react' import { GlobalProvider } from './components' import { Home } from './Home' export function App() { return <GlobalProvider><Home /></GlobalProvider> }
如今能夠在 Home 組件裏面消費 store:npm
// Home.tsx import React from 'react' import { useGlobal } from './store' export default function Home() { const { currentUser } = useGlobal() const [user, dispatch] = currentUser // 使用 dispatch 修改用戶姓名 function changeName(event) { dispatch({ type: 'update_name', payload: event.target.value }) } return <div> <h2>{user.name}</h2> <input onChange={changeName} /> </div> }
下面是定義 reducer 的 User.ts 代碼:redux
// User.ts function init() { return { name: '' } } const reducer = (state, { type, payload }) => { switch(type) { case 'UPDATE_NAME': { return { ...state, name: payload } } case 'UPDATE': { return { ...state, ...payload } } case 'INIT': { return init() } default: { return state } } } export default { init, reducer }
上面的 store 已經可以正常運做了,可是還有優化空間,咱們再次聚焦到 Home.tsx 上,有些同窗應該已經發現了問題:數據結構
// Home.tsx import React from 'react' import { useGlobal } from './store' export default function Home() { const { currentUser } = useGlobal() const [user, dispatch] = currentUser function changeName(event) { dispatch({ // 這裏的 update_name 與 User.ts 中定義的 UPDATE_NAME 不一致 // 這是個 bug,可是隻有在運行時會報錯 type: 'update_name', payload: event.target.value }) } return <div> <h2>{user.name}</h2> <input onChange={changeName} /> </div> }
咱們須要加一些類型提示🤔react-router
咱們先肯定下指望,咱們但願 reducer typing 能帶給咱們的提示有:
上面的粗體部分是咱們指望 typing 提供的 4 個功能,讓咱們嘗試解決一下:
// User.ts // IUser 做爲用戶數據結構類型 type IUser = { name: string } function init() { return { name: '', } } // 使用 Union Types 枚舉全部的 action type ActionType = | { type: 'INIT' payload: void } | { type: 'UPDATE_NAME' payload: string } | { type: 'UPDATE' payload: IUser } const reducer = (state, { type, payload }: ActionType) => { switch (type) { case 'UPDATE_NAME': { return { ...state, name: payload } } case 'UPDATE': { return { ...state, ...payload } } case 'INIT': { return init() } default: { return state } } } export default { init, reducer, }
看起來好像很不錯,可是。。。
typescript 把 type 和 payload 兩個字段分別作了 union,致使對象展開符報錯了。
咱們能夠把 payload 定義成 any 解決這個問題,可是就會失去上面指望的對於 paylod 的類型提示。
一種更健壯的方案是使用 Type Guard,代碼看起來會像下面這樣:
// User.ts type IUser = { name: string } function init() { return { name: '', } } type InitAction = { type: 'INIT' payload: void } type UpdateNameAction = { type: 'UPDATE_NAME' payload: string } type UpdateAction = { type: 'UPDATE' payload: IUser } type ActionType = InitAction | UpdateNameAction | UpdateAction function isInitAction(action): action is InitAction { return action.type === 'INIT' } function isUpdateNameAction(action): action is UpdateNameAction { return action.type === 'UPDATE_NAME' } function isUpdateAction(action): action is UpdateAction { return action.type === 'UPDATE' } const reducer = (state, action: ActionType) => { if (isUpdateNameAction(action)) { return { ...state, name: action.payload, } } if (isUpdateAction(action)) { return { ...state, ...action.payload, } } if (isInitAction(action)) { return init() } return state } export default { init, reducer, }
咱們獲得了咱們想要的:
可是每寫一個 action 都要加一個 guard,太浪費寶貴的時間了。讓咱們用 deox 這個庫優化一下咱們的代碼:
// User.ts import { createReducer, createActionCreator } from 'deox' type IUser = { name: string } function init() { return { name: '', } } const reducer = createReducer(init(), action => [ action(createActionCreator('INIT'), () => init()), action( createActionCreator('UPDATE', resolve => (payload: IUser) => resolve(payload) ), (state, { payload }) => ({ ...state, ...payload, }) ), action( createActionCreator('UPDATE_NAME', resolve => (payload: string) => resolve(payload) ), (state, { payload }) => ({ ...state, name: payload, }) ), ]) export default { init, reducer, }
讓咱們再作一些小小的優化,把繁重的 createActionCreator 函數簡化一下:
// User.ts import { createReducer, createActionCreator } from 'deox' type IUser = { name: string } function init() { return { name: '', } } // 簡化之後的 createAction 須要調用兩次 // 第一次調用使用類型推斷出 action type // 第二次調用使用範型聲明 payload type function createAction<K extends string>(name: K) { return function _createAction<T = void, M = void>() { return createActionCreator(name, resolve => (payload: T, meta?: M) => resolve(payload, meta) ) } } const reducer = createReducer(init(), action => [ action(createAction('INIT')(), () => init()), action(createAction('UPDATE')<IUser>(), (state, { payload }) => ({ ...state, ...payload, })), action(createAction('UPDATE_NAME')<string>(), (state, { payload }) => ({ ...state, name: payload, })), ]) export default { init, reducer, }
大功告成,能夠開始愉快地寫代碼了😊
開發前端頁面的時候會遇到不少頁面自適應的需求,這個時候子組件就須要根據父組件的寬高來調整本身的尺寸,咱們能夠開發一個獲取父容器 boundingClientRect 的 hook,獲取 boundingClientRect 在 React 官網有介紹:
function useClientRect() { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }
咱們擴展一下讓它支持監聽頁面自適應:
// useLayoutRect.ts import { useState, useEffect, useRef } from 'react' export function useLayoutRect(): [ ClientRect, React.MutableRefObject<any> ] { const [rect, setRect] = useState({ width: 0, height: 0, left: 0, top: 0, bottom: 0, right: 0, }) const ref = useRef(null) const getClientRect = () => { if (ref.current) { setRect(ref.current.getBoundingClientRect()) } } // 監聽 window resize 更新 rect useEffect(() => { getClientRect() window.addEventListener('resize', getClientRect) return () => { window.removeEventListener('resize', getClientRect) } }, []) return [rect, ref] }
你能夠這麼使用:
export function ResizeDiv() { const [rect, ref] = useLayoutRect() return <div ref={ref}>The width of div is {rect.width}px</div> }
若是你的需求只是監聽 window resize 的話,這個 hook 寫到這裏就能夠了,可是若是你須要監聽其餘會引發界面尺寸變動的事件(好比菜單的伸縮)時要怎麼辦?讓咱們改造一下 useLayoutRect 讓它更加靈活:
// useLayoutRect.ts import { useState, useEffect, useRef } from 'react' export function useLayoutRect(): [ ClientRect, React.MutableRefObject<any>, () => void ] { ... const getClientRect = () => { if (ref.current) { setRect(ref.current.getBoundingClientRect()) } } ... // 額外導出 getClientRect 方法 return [rect, ref, getClientRect] }
上面的代碼咱們多導出了 getClientRect 方法,而後咱們能夠組合成新的 useResize hook:
// useResize.ts import { useLayoutRect } from '@/utils' import { useGlobal } from './store' import { useEffect } from 'react' export function useResize(): [ClientRect, React.MutableRefObject<any>] { // menuExpanded 存在 global store 中,用於獲取菜單的伸縮狀態 const { env } = useGlobal() const [{ menuExpanded }] = env const [rect, ref, resize] = useLayoutRect() // 由於菜單伸縮有 300ms 的動畫,咱們須要加個延時 useEffect(() => { setTimeout(resize, 300) }, [menuExpanded]) return [rect, ref] }
你能夠在 useResize 中添加其餘的業務邏輯,hooks 具備很好的組合性和靈活性👍
Charles Stover 在 git 上發佈了一個很好用的 react-router hook,能夠在 functional component 中實現 withRouter 的功能。實現的原理能夠參考他的這篇博客 How to convert withRouter to a react hook。使用方法以下:
import useReactRouter from 'use-react-router'; const MyPath = () => { const { history, location, match } = useReactRouter(); return ( <div> My location is {location.pathname}! </div> ) }
學習 react hooks 的過程當中愈發以爲 hooks 的強大,也更加能理解爲何說 react hooks 是 future。相信隨着時間的發展,社區可以創造出愈來愈的 custom hook,也會涌現出愈來愈多像 hooks 這樣開創性的設計。