react-hooks toolkit

        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

State Management

        以前 React 對於狀態管理沒有提供很好的支持,因此咱們會依賴 reduxmobx 這些第三方的狀態管理庫。redux 很好地實踐了 immutable 和 pure function;mobx 則是 mutable 和 reactive 的表明。如今使用 React 內置 useReduceruseContext 這兩個 hook 可讓咱們實現 redux 風格的狀態管理;結合 useMemouseEffect 能夠模擬 mobx 的 computedreaction。(hooks 沒有使用 Proxy,因此跟 mobx 的代理響應是不同的)
        下面咱們使用 hooks 實現一個原生的狀態管理庫。前端

Global Store

        跟 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

  1. GlobalContext:全局 context,用於連接 store 和相關組件;
  2. useGlobalStore:自定義 hook,將多個數據模塊封裝在一塊兒,用於 GlobalContext.Provider 的 value 屬性;
  3. useGlobal:自定義 hook,用於子組件引用全局 store;

Provider

        下面是使用 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>
}

Consumer

        如今能夠在 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

        下面是定義 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
}

Reducer Typing

        上面的 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

Have a try

        咱們先肯定下指望,咱們但願 reducer typing 能帶給咱們的提示有:

  1. 在 Consumer 中調用 dispatch,輸入 type 之後會顯示該 reducer 相關的全部 action type 名稱,輸入不存在的 type 會有 錯誤提示
  2. 在第一步輸入正確的 type 之後,再輸入 payload,會有對應 type 的 payload 的類型提示,輸入不一致的 payload 會有 錯誤提示

        上面的粗體部分是咱們指望 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,
}

        看起來好像很不錯,可是。。。

屏幕快照 2020-01-06 下午6.02.01.png

        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,
}

        咱們獲得了咱們想要的:

  1. 輸入 type 之後會顯示該 reducer 相關的全部 action type 名稱
    屏幕快照 2020-01-06 下午6.27.52.png
  2. 輸入不存在的 type 會有 錯誤提示
    屏幕快照 2020-01-06 下午6.28.26.png
  3. payload 的類型提示
    屏幕快照 2020-01-06 下午7.14.00.png
  4. 輸入不一致的 payload 會有 錯誤提示
    屏幕快照 2020-01-06 下午7.14.51.png

        可是每寫一個 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,
}

        大功告成,能夠開始愉快地寫代碼了😊

useResize

        開發前端頁面的時候會遇到不少頁面自適應的需求,這個時候子組件就須要根據父組件的寬高來調整本身的尺寸,咱們能夠開發一個獲取父容器 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 具備很好的組合性和靈活性👍

useReactRouter

        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 這樣開創性的設計。

相關文章
相關標籤/搜索