2019年了,整理了N個實用案例幫你快速遷移到React Hooks(收藏慢慢看系列)

React Conf 2018宣佈React Hooks後,我第一時間開始嘗試使用React Hooks,如今新項目基本不寫Class組件了。對我來講,它確實讓個人開發效率提升了不少,改變了已有的組件開發思惟和模式.html

我在React組件設計實踐總結04 - 組件的思惟中已經總結過React Hooks的意義,以及一些應用場景vue

那這篇文章就徹底是介紹React Hooks的應用實例,列舉了我使用React Hooks的一些實踐。 但願經過這些案例,能夠幫助你快速熟練,並遷移到React Hooks開發模式.react

文章篇幅很長,建議收藏不看, 至少看看目錄吧git


把以前文章整理的React Hooks應用場景總結拿過來, 本文基本按照這個範圍進行組織:程序員


若是你想要了解React Hooks的原理能夠閱讀這些文章:github


目錄索引web


1. 組件狀態

React提供了一個很基本的組件狀態設置Hook:spring

const [state, setState] = useState(initialState);
複製代碼

useState返回一個state,以及更新state的函數. setState能夠接受一個新的值,會觸發組件從新渲染.shell

React會確保setState函數是穩定的,不會在組件從新渲染時改變。下面的useReducer的dispatch函數、useRef的current屬性也同樣。 這就意味着setState、dispatch、ref.current, 能夠安全地在useEffect、useMemo、 useCallback中引用npm


1-1 useSetState 模擬傳統的setState

useState和Class組件的setState不太同樣.

Class組件的state屬性通常是一個對象,調用setState時,會淺拷貝到state屬性, 並觸發更新, 好比:

class MyComp extends React.Component {
  state = {
    name: '_sx_',
    age: 10
  }

  handleIncrementAge = () => {
    // 只更新age
    this.setState({age: this.state.age + 1})
  }

  // ...
}
複製代碼

useState會直接覆蓋state值。爲了實現和setState同樣的效果, 能夠這樣子作:

const initialState = {name: 'sx', age: 10}
const MyComp: FC = props => {
  const [state, setState] = useState(initialState)
  const handleIncrementAge = useCallback(() => {
    // setState方法支持接收一個函數,經過這個函數能夠獲取最新的state值
    // 而後使用...操做符實現對象淺拷貝
    setState((prevState) => ({...preState, age: prevState.age + 1}) )
  }, [])
  // ...
}
複製代碼

Ok,如今把它封裝成通用的hooks,在其餘組件中複用。這時候就體現出來Hooks強大的邏輯抽象能力:Hooks 旨在讓組件的內部邏輯組織成可複用的更小單元,這些單元各自維護一部分組件‘狀態和邏輯’

看看咱們的useSetState, 我會使用Typescript進行代碼編寫:

function useSetState<S extends object>(
  initalState: S | (() => S),
): [S, (state: Partial<S> | ((state: S) => Partial<S>)) => void] {
  const [_state, _setState] = useState<S>(initalState)

  const setState = useCallback((state: Partial<S> | ((state: S) => Partial<S>)) => {
    _setState((prev: S) => {
      let nextState = state
      if (typeof state === 'function') {
        nextState = state(prev)
      }

      return { ...prev, ...nextState }
    })
  }, [])

  return [_state, setState]
}

// ------
// EXAMPLE
// ------
export default function UseSetState() {
  const [state, setState] = useSetState<{ name: string; age: number }>({ name: 'sx', age: 1 })

  const incrementAge = () => {
    setState(prev => ({ age: prev.age + 1 }))
  }

  return (
    <div onClick={incrementAge}>
      {state.name}: {state.age}
    </div>
  )
}
複製代碼

hooks命名以use爲前綴


⤴️回到頂部

1-2 useReducer Redux風格狀態管理

若是組件狀態比較複雜,推薦使用useReducer來管理狀態。若是你熟悉Redux,會很習慣這種方式。

// 定義初始狀態
const initialState = {count: 0};

// 定義Reducer
// ruducer接受當前state,以及一個用戶操做,返回一個'新'的state
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

// --------
// EXAMPLE
// --------
function Counter() {
  // 返回state,以及dispatch函數
  // dispatch函數能夠觸發reducer執行,給reducer傳遞指令和載荷
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <> Count: {state.count} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } 複製代碼

瞭解更多reducer的思想能夠參考Redux文檔


⤴️回到頂部

1-3 useForceUpdate 強制從新渲染

Class組件能夠經過forceUpdate實例方法來觸發強制從新渲染。使用useState也能夠模擬相同的效果:

export default function useForceUpdate() {
  const [, setValue] = useState(0)
  return useCallback(() => {
    // 遞增state值,強制React進行從新渲染
    setValue(val => (val + 1) % (Number.MAX_SAFE_INTEGER - 1))
  }, [])
}

// -------
// EXAMPLE
// -------
function ForceUpdate() {
  const forceUpdate = useForceUpdate()
  useEffect(() => {
    somethingChange(forceUpdate)
  }, [])
}
複製代碼

⤴️回到頂部

1-4 useStorage 簡化localStorage存取

經過自定義Hooks,能夠將狀態代理到其餘數據源,好比localStorage。 下面案例展現若是使用Hooks封裝和簡化localStorage的存取:

import { useState, useCallback, Dispatch, SetStateAction } from 'react'

export default function useStorage<T>(
  key: string,
  // 默認值
  defaultValue?: T | (() => T),
  // 是否在窗口關閉後保持數據
  keepOnWindowClosed: boolean = true,
): [T | undefined, Dispatch<SetStateAction<T>>, () => void] {
  const storage = keepOnWindowClosed ? localStorage : sessionStorage

  // 嘗試從Storage恢復值
  const getStorageValue = () => {
    try {
      const storageValue = storage.getItem(key)
      if (storageValue != null) {
        return JSON.parse(storageValue)
      } else if (defaultValue) {
        // 設置默認值
        const value = typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue
        storage.setItem(key, JSON.stringify(value))
        return value
      }
    } catch (err) {
      console.warn(`useStorage 沒法獲取${key}: `, err)
    }

    return undefined
  }

  const [value, setValue] = useState<T | undefined>(getStorageValue)

  // 更新組件狀態並保存到Storage
  const save = useCallback<Dispatch<SetStateAction<T>>>(value => {
    setValue(prev => {
      const finalValue = typeof value === 'function' ? (value as (prev: T | undefined) => T)(prev) : value
      storage.setItem(key, JSON.stringify(finalValue))
      return finalValue
    })
  }, [])

  // 移除狀態
  const clear = useCallback(() => {
    storage.removeItem(key)
    setValue(undefined)
  }, [])

  return [value, save, clear]
}

// --------
// EXAMPLE
// --------
function Demo() {
  // 保存登陸狀態
  const [use, setUser, clearUser] = useStorage('user')
  const handleLogin = (user) => {
    setUser(user)
  }

  const handleLogout = () => {
    clearUser()
  }

  // ....
}
複製代碼

⤴️回到頂部

1-5 useRefState 引用state的最新值



上圖是今年六月份VueConf,尤雨溪的Slide截圖,他對比了Vue最新的FunctionBase API和React Hook. 它指出React Hooks有不少問題:

  • 每一個Hooks在組件每次渲染時都執行。也就是說每次渲染都要從新建立閉包和對象
  • 須要理解閉包變量
  • 內容回調/對象會致使純組件props比對失效, 致使組件永遠更新

閉包變量問題是你掌握React Hooks過程當中的重要一關。閉包問題是指什麼呢?舉個簡單的例子, Counter:

function Counter() {
  const [count, setCount] = useState(0)
  const handleIncr = () => {
    setCount(count + 1)
  }

  return (<div>{count}: <ComplexButton onClick={handleIncr}>increment</ComplexButton></div>)
}
複製代碼

假設ComplexButton是一個很是複雜的組件,每一次點擊它,咱們會遞增count,從而觸發組將從新渲染。由於Counter每次渲染都會從新生成handleIncr,因此也會致使ComplexButton從新渲染,無論ComplexButton使用了PureComponent仍是使用React.memo包裝


爲了解決這個問題,React也提供了一個useCallback Hook, 用來‘緩存’函數, 保持回調的不變性. 好比咱們能夠這樣使用:

function Counter() {
  const [count, setCount] = useState(0)
  const handleIncr = useCallback(() => {
    setCount(count + 1)
  }, [])

  return (<div>{count}: <ComplexButton onClick={handleIncr}>increment</ComplexButton></div>)
}
複製代碼

上面的代碼是有bug的,不過怎麼點擊,count會一直顯示爲1!

再仔細閱讀useCallback的文檔,useCallback支持第二個參數,當這些值變更時更新緩存的函數, useCallback的內部邏輯大概是這樣的

let memoFn, memoArgs
function useCallback(fn, args) {
  // 若是變更則更新緩存函數
  if (!isEqual(memoArgs, args)) {
    memoArgs = args
    return (memoFn = fn)
  }
  return memoFn
}
複製代碼

Ok, 如今理解一下爲何會一直顯示1?

首次渲染時緩存了閉包,這時候閉包捕獲的count值是0。在後續的從新渲染中,由於useCallback第二個參數指定的值沒有變更,handleIncr閉包會永遠被緩存。這就解釋了爲何每次點擊,count只能爲1.

解決辦法也很簡單,讓咱們在count變更時,讓useCallback更新緩存函數:

function Counter() {
  const [count, setCount] = useState(0)
  const handleIncr = useCallback(() => {
    setCount(count + 1)
  }, [count])

  return (<div>{count}: <ComplexButton onClick={handleIncr}>increment</ComplexButton></div>)
}
複製代碼

若是useCallback依賴不少值,你的代碼多是這樣的:useCallback(fn, [a, b, c, d, e]). 反正我是沒法接受這種代碼的,很容易遺漏, 並且可維護性不好,儘管經過ESLint插件能夠檢查這些問題**。


其實經過useRef Hook,可讓咱們像Class組件同樣保存一些‘實例變量’, React會保證useRef返回值的穩定性,咱們能夠在組件任何地方安全地引用ref。

基於這個原理,咱們嘗試封裝一個useRefState, 它在useState的基礎上擴展了一個返回值,用於獲取state的最新值:


import { useState, useRef, useCallback, Dispatch, SetStateAction, MutableRefObject } from 'react'

function useRefState(initialState) {
  const ins = useRef()

  const [state, setState] = useState(() => {
    // 初始化
    const value = typeof initialState === 'function' ? initialState() : initialState
    ins.current = value
    return value
  })

  const setValue = useCallback(value => {
    if (typeof value === 'function') {
      setState(prevState => {
        const finalValue = value(prevState)
        ins.current = finalValue
        return finalValue
      })
    } else {
      ins.current = value
      setState(value)
    }
  }, [])

  return [state, setValue, ins]
}
複製代碼

使用示例:

function Counter() {
  const [count, setCount, countRef] = useRefState(0)
  const handleIncr = useCallback(() => {
    setCount(countRef.current + 1)
  }, [])

  useEffect(() => {
    return () => {
      // 在組件卸載時保存當前的count
      saveCount(countRef.current)
    }
  }, [])

  return (<div>{count}: <ComplexButton onClick={handleIncr}>increment</ComplexButton></div>)
}
複製代碼

useEffectuseMemouseCallback同樣存在閉包變量問題,它們和useCallback一個支持指定第二個參數,當這個參數變化時執行反作用。


⤴️回到頂部

1-5-1 每次從新渲染都建立閉包會影響效率嗎?

函數組件和Class組件不同的是,函數組件將全部狀態和邏輯都放到一個函數中, 每一次從新渲染會重複建立大量的閉包、對象。而傳統的Class組件的render函數則要簡潔不少,通常只放置JSX渲染邏輯。相比你們都跟我同樣,會懷疑函數組件的性能問題

咱們看看官方是怎麼迴應的:


我在SegmentFault的react function組件與class組件性能問題也進行了詳細的回答, 結論是:

目前而言,實現一樣的功能,類組件和函數組件的效率是不相上下的。可是函數組件是將來,並且還有優化空間,React團隊會繼續優化它。而類組件會逐漸退出歷史

爲了提升函數組件的性能,能夠在這些地方作一些優化:

  • 可否將函數提取爲靜態的

    // 1️⃣例如將不依賴於組件狀態的回調抽取爲靜態方法
    const goback = () => {
      history.go(-1)
    }
    
    function Demo() {
      //const goback = () => {
      // history.go(-1)
      //}
      return <button onClick={goback}>back</button>
    }
    
    // 2️⃣ 抽離useState的初始化函數
    const returnEmptyObject = () => Object.create(null)
    const returnEmptyArray = () => []
    function Demo() {
      const [state, setState] = useState(returnEmptyObject)
      const [arr, setArr] = useState(returnEmptyArray)
      // ...
    }
    複製代碼
  • 簡化組件的複雜度,動靜分離

  • 再拆分更細粒度的組件,這些組件使用React.memo緩存



⤴️回到頂部

1-6 useRefProps 引用最新的Props

現實項目中也有不少這種場景: 咱們想在組件的任何地方獲取最新的props值,這個一樣能夠經過useRef來實現:

export default function useRefProps<T>(props: T) {
  const ref = useRef<T>(props)
  // 每次從新渲染設置值
  ref.current = props

  return ref
}

// ---------
// EXAMPLE
// ---------
function MyButton(props) {
  const propsRef = useRefProps(props)

  // 永久不變的事件處理器
  const handleClick = useCallback(() => {
    const { onClick } = propsRef.current
    if (onClick) {
      onClick()
    }
  }, [])

  return <ComplexButton onClick={handleClick}></ComplexButton>
}
複製代碼

⤴️回到頂部

1-7 useInstance ‘實例’變量存取

function isFunction<T>(initial?: T | (() => T)): initial is () => T {
  return typeof initial === 'function'
}

function useInstance<T extends {}>(initial?: T | (() => T)) {
  const instance = useRef<T>()
  // 初始化
  if (instance.current == null) {
    if (initial) {
      instance.current = isFunction(initial) ? initial() : initial
    } else {
      instance.current = {} as T
    }
  }

  return instance.current
}

// ---------
// EXAMPLE
// ---------
function Demo() {
  const inst = useInstance({ count: 1 })
  const update = useForceUpdate()
  useEffect(() => {
    const timer = setInterval(() => {
      // 像類組件同樣,進行‘實例變量’存儲
      // 在函數組件的任意地方引用
      // 只不過更新這些數據不會觸發組件的從新渲染
      inst.count++
    }, 1000)

    return () => clearInterval(timer)
  }, [])

  return (
    <div>
      count: {inst.count}
      <button onClick={update}>刷新</button>
    </div>
  )
}
複製代碼

注意不要濫用


⤴️回到頂部

1-9 usePrevious 獲取上一次渲染的值

在Class組件中,咱們常常會在shouldComponentUpdatecomponentDidUpdate這類生命週期方法中對props或state進行比對,來決定作某些事情,例如從新發起請求、監聽事件等等.

Hooks中咱們可使用useEffect或useMemo來響應狀態變化,進行狀態或反作用衍生. 因此上述比對的場景在Hooks中不多見。但也不是不可能,React官方案例中就有一個usePrevious:

function usePrevious(value) {
  const ref = useRef();
  // useEffect會在完成此次'渲染'以後執行
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

// --------
// EXAMPLE
// --------
const calculation = count * 100;
const prevCalculation = usePrevious(calculation);
複製代碼

⤴️回到頂部

1-10 useImmer 簡化不可變數據操做

這個案例來源於use-immer, 結合immer.js和Hooks來簡化不可變數據操做, 看看代碼示例:

const [person, updatePerson] = useImmer({
  name: "Michel",
  age: 33
});

function updateName(name) {
  updatePerson(draft => {
    draft.name = name;
  });
}

function becomeOlder() {
  updatePerson(draft => {
    draft.age++;
  });
}
複製代碼

實現也很是簡單:

export function useImmer(initialValue) {
  const [val, updateValue] = useState(initialValue);
  return [
    val,
    useCallback(updater => {
      updateValue(produce(updater));
    }, [])
  ];
}
複製代碼

簡潔的Hooks配合簡潔的Immer,簡直完美


⤴️回到頂部

1-11 封裝'工具Hooks'簡化State的操做

Hooks只是普通函數,因此能夠靈活地自定義。下面舉一些例子,利用自定義Hooks來簡化常見的數據操做場景


1-11-1 useToggle 開關

實現boolean值切換

function useToggle(initialValue?: boolean) {
  const [value, setValue] = useState(!!initialValue)
  const toggler = useCallback(() => setValue(value => !value), [])

  return [value, toggler]
}

// --------
// EXAMPLE
// --------
function Demo() {
  const [enable, toggleEnable] = useToggle()

  return <Switch value={enable} onClick={toggleEnable}></Switch>
}
複製代碼

⤴️回到頂部

1-11-2 useArray 簡化數組狀態操做

function useArray<T>(initial?: T[] | (() => T[]), idKey: string = 'id') {
  const [value, setValue] = useState(initial || [])
  return {
    value,
    setValue,
    push: useCallback(a => setValue(v => [...v, a]), []),
    clear: useCallback(() => setValue(() => []), []),
    removeById: useCallback(id => setValue(arr => arr.filter(v => v && v[idKey] !== id)), []),
    removeIndex: useCallback(
      index =>
        setValue(v => {
          v.splice(index, 1)
          return v
        }),
      [],
    ),
  }
}

// ---------
// EXAMPLE
// ---------
function Demo() {
  const {value, push, removeById} = useArray<{id: number, name: string}>()
  const handleAdd = useCallback(() => {
    push({id: Math.random(), name: getName()})
  }, [])

  return (<div>
    <div>{value.map(i => <span key={i.id} onClick={() => removeById(i.id)}>{i.name}</span>)}</div>
    <button onClick={handleAdd}>add</button>
  </div>)
}
複製代碼

限於篇幅,其餘數據結構, 例如Set、Map, 就不展開介紹了,讀者能夠本身發揮想象力.



⤴️回到頂部

2. 模擬生命週期函數

組件生命週期相關的操做依賴於useEffect Hook. React在函數組件中刻意淡化了組件生命週期的概念,而更關注‘數據的響應’.

useEffect名稱意圖很是明顯,就是專門用來管理組件的反作用。和useCallback同樣,useEffect支持傳遞第二個參數,告知React在這些值發生變更時才執行父做用. 原理大概以下:

let memoCallback = {fn: undefined, disposer: undefined}
let memoArgs
function useEffect(fn, args) {
  // 若是變更則執行反作用
  if (args == null || !isEqual(memoArgs, args)) {
    memoArgs = args
    memoCallback.fn = fn

    // 放進隊列等待調度執行
    pushIntoEffectQueue(memoCallback)
  }
}

// 反作用執行
// 這個會在組件完成渲染,在佈局(layout)和繪製(paint)以後被執行
// 若是是useLayoutEffect, 執行的時機會更早一些
function queueExecute(callback) {
  // 先執行清理函數
  if (callback.disposer) {
    callback.disposer()
  }
  callback.disposer = callback.fn()
}
複製代碼

關於useEffect官網有詳盡的描述; Dan Abramov也寫了一篇useEffect 完整指南, 推薦👍。


⤴️回到頂部

2-1 useOnMount 模擬componentDidMount

export default function useOnMount(fn: Function) {
  useEffect(() => {
    fn()
  }, []) // 第二個參數設置爲[], 表示沒必要對任何數據, 因此只在首次渲染時調用
}

// ---------
// EXAMPLE
// ---------
function Demo() {
  useOnMount(async () => {
    try {
      await loadList()
    } catch {
      // log
    }
  })
}
複製代碼

若是須要在掛載/狀態更新時請求一些資源、而且須要在卸載時釋放這些資源,仍是推薦使用useEffect,由於這些邏輯最好放在一塊兒, 方便維護和理解:

// 可是useEffect傳入的函數不支持async/await(返回Promise)
useEffect(() => {
  // 請求資源
  const subscription = props.source.subscribe();

  // 釋放資源
  return () => {
    subscription.unsubscribe();
  };
}, []);
複製代碼

⤴️回到頂部

2-2 useOnUnmount 模擬componentWillUnmount

export default function useOnUnmount(fn: Function) {
  useEffect(() => {
    return () => {
        fn()
    }
  }, [])
}
複製代碼

⤴️回到頂部

2-3 useOnUpdate 模擬componentDidUpdate

function useOnUpdate(fn: () => void, dep?: any[]) {
  const ref = useRef({ fn, mounted: false })
  ref.current.fn = fn

  useEffect(() => {
    // 首次渲染不執行
    if (!ref.current.mounted) {
      ref.current.mounted = true
    } else {
      ref.current.fn()
    }
  }, dep)
}

// -----------
// EXAMPLE
// -----------
function Demo(props) {
  useOnUpdate(() => {
    dosomethingwith(props.a)
  }, [props.a])

  return <div>...</div>
}
複製代碼

其餘生命週期函數的模擬:

  • shouldComponentUpdate - React.memo包裹組件
  • componentDidCatch - 暫不支持


⤴️回到頂部

3. 事件處理

3-1 useChange 簡化onChange表單雙向綁定

表單值的雙向綁定在項目中很是常見,一般咱們的代碼是這樣的:

function Demo() {
  const [value, setValue] = useState('')

  const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(evt => {
    setValue(evt.target.value)
  }, [])

  return <input value={value} onChange={handleChange} />
}
複製代碼

若是須要維護多個表單,這種代碼就會變得難以接受。幸虧有Hooks,咱們能夠簡化這些代碼:

function useChange<S>(initial?: S | (() => S)) {
  const [value, setValue] = useState<S | undefined>(initial)
  const onChange = useCallback(e => setValue(e.target.value), [])

  return {
    value,
    setValue,
    onChange,
    // 綁定到原生事件
    bindEvent: {
      onChange,
      value,
    },
    // 綁定到自定義組件
    bind: {
      onChange: setValue,
      value,
    },
  }
}

// ----------
// EXAMPLE
// ----------
function Demo() {
  const userName = useChange('')
  const password = useChange('')

  return (
    <div>
      <input {...userName.bindEvent} />
      <input type="password" {...password.bindEvent} />
    </div>
  )
}
複製代碼

⤴️回到頂部

3-2 useBind 綁定回調參數

綁定一些回調參數,並利用useMemo給下級傳遞一個緩存的回調, 避免從新渲染:

function useBind(fn?: (...args: any[]) => any, ...args: any[]): (...args: any[]) => any {
  return useMemo(() => {fn && fn.bind(null, ...args)}, args)
}

// ---------
// EXAMPLE
// ---------
function Demo(props) {
  const {id, onClick} = props
  const handleClick = useBind(onClick, id)

  return <ComplexComponent onClick={handleClick}></ComplexComponent>
}

// 等價於
function Demo(props) {
  const {id, onClick} = props
  const handleClick = useCallback(() => onClick(id), [id])

  return <ComplexComponent onClick={handleClick}></ComplexComponent>
}
複製代碼

⤴️回到頂部

3-3 自定義事件封裝

Hooks也能夠用於封裝一些高級事件或者簡化事件的處理,好比拖拽、手勢、鼠標Active/Hover等等;


3-3-1 useActive

舉個簡單的例子, useActive, 在鼠標按下時設置狀態爲true,鼠標釋放時恢復爲false:

function useActive(refEl: React.RefObject<HTMLElement>) {
  const [value, setValue] = useState(false)
  useEffect(() => {
    const handleMouseDown = () => {
      setValue(true)
    }
    const handleMouseUp = () => {
      setValue(false)
    }

    // DOM 事件監聽
    if (refEl && refEl.current) {
      refEl.current.addEventListener('mousedown', handleMouseDown)
      refEl.current.addEventListener('mouseup', handleMouseUp)
    }

    return () => {
      if (refEl && refEl.current) {
        refEl.current.removeEventListener('mousedown', handleMouseDown)
        refEl.current.removeEventListener('mouseup', handleMouseUp)
      }
    }
  }, [])

  return value
}

// ----------
// EXAMPLE
// ----------
function Demo() {
  const elRef = useRef(null)
  const active = useActive(inputRef)

  return (<div ref={elRef}>{active ? "Active" : "Nop"}</div>)
}
複製代碼

⤴️回到頂部

3-3-2 useTouch 手勢事件封裝

更復雜的自定義事件, 例如手勢。限於篇幅就不列舉它們的實現代碼,咱們能夠看看它們的Demo:

function Demo() {
  const {ref} = useTouch({
    onTap: () => { /* 點擊 */ },
    onLongTap: () => { /* 長按 */ },
    onRotate: () => {/* 旋轉 */}
    // ...
  })

  return (<div className="box" ref={ref}></div>)
}
複製代碼

useTouch的實現能夠參考useTouch.ts


⤴️回到頂部

3-3-3 useDraggable 拖拽事件封裝

拖拽也是一個典型的自定義事件, 下面這個例子來源於這裏

function useDraggable(ref: React.RefObject<HTMLElement>) {
  const [{ dx, dy }, setOffset] = useState({ dx: 0, dy: 0 })

  useEffect(() => {
    if (ref.current == null) {
      throw new Error(`[useDraggable] ref未註冊到組件中`)
    }
    const el = ref.current

    const handleMouseDown = (event: MouseEvent) => {
      const startX = event.pageX - dx
      const startY = event.pageY - dy

      const handleMouseMove = (event: MouseEvent) => {
        const newDx = event.pageX - startX
        const newDy = event.pageY - startY
        setOffset({ dx: newDx, dy: newDy })
      }

      document.addEventListener('mousemove', handleMouseMove)
      document.addEventListener(
        'mouseup',
        () => {
          document.removeEventListener('mousemove', handleMouseMove)
        },
        { once: true },
      )
    }

    el.addEventListener('mousedown', handleMouseDown)

    return () => {
      el.removeEventListener('mousedown', handleMouseDown)
    }
  }, [dx, dy])

  useEffect(() => {
    if (ref.current) {
      ref.current.style.transform = `translate3d(${dx}px, ${dy}px, 0)`
    }
  }, [dx, dy])
}

// -----------
// EXAMPLE
// -----------
function Demo() {
  const el = useRef();
  useDraggable(el);

  return <div className="box" ref={el} />
}
複製代碼

可運行例子


⤴️回到頂部

3-3-4 react-events 面向將來的高級事件封裝

我在<談談React事件機制和將來(react-events)>介紹了React-Events這個實驗性的API。當這個API成熟後,咱們能夠基於它來實現更優雅的高級事件的封裝:

import { PressResponder, usePressListener } from 'react-events/press';

const Button = (props) => (
  const listener = usePressListener({  // ⚛️ 經過hooks建立Responder
    onPressStart,
    onPress,
    onPressEnd,
  })

  return (
    <div listeners={listener}> {subtrees} </div>
  );
);
複製代碼

⤴️回到頂部

3-4 useSubscription 通用事件源訂閱

React官方維護了一個use-subscription包,支持使用Hooks的形式來監聽事件源. 事件源能夠是DOM事件、RxJS的Observable等等.

先來看看使用示例:

// 監聽rxjs behaviorSubject
function Demo() {
  const subscription = useMemo(
    () => ({
      getCurrentValue: () => behaviorSubject.getValue(),
      subscribe: callback => {
        // 當事件觸發時調用callback
        const subscription = behaviorSubject.subscribe(callback);
        // 和useEffect同樣,返回一個函數來取消訂閱
        return () => subscription.unsubscribe();
      }
    }),
    // 在behaviorSubject變化後從新訂閱
    [behaviorSubject]
  );

  const value = useSubscription(subscription);

  return <div>{value}</div>
}
複製代碼

如今來看看實現:

export function useSubscription<T>({
  getCurrentValue,
  subscribe,
}: {
  // 獲取當前值
  getCurrentValue?: () => T
  // 用於訂閱事件源
  subscribe: (callback: Function) => () => void
}): T {
  const [state, setState] = useState(() => ({ getCurrentValue, subscribe, value: getCurrentValue() }))
  let valueToReturn = state.value

  // 更新getCurrentValue和subscribe
  if (state.getCurrentValue !== getCurrentValue || state.subscribe !== subscribe) {
    valueToReturn = getCurrentValue()
    setState({ getCurrentValue, subscribe, value: valueToReturn })
  }

  useEffect(() => {
    let didUnsubscribe = false
    const checkForUpdates = () => {
      if (didUnsubscribe) {
        return
      }

      setState(prevState => {
        // 檢查getCurrentValue和subscribe是否變更
        // setState時若是返回值沒有變化,則不會觸發從新渲染
        if (prevState.getCurrentValue !== getCurrentValue || prevState.subscribe !== subscribe) {
          return prevState
        }
        // 值沒變更
        const value = getCurrentValue()
        if (prevState.value === value) {
          return prevState
        }

        return { ...prevState, value }
      })
    }
    const unsubscribe = subscribe(checkForUpdates)
    checkForUpdates()

    return () => {
      didUnsubscribe = true
      unsubscribe()
    }
  }, [getCurrentValue, subscribe])

  return valueToReturn
}
複製代碼

實現也不復雜,甚至能夠說有點囉嗦.


⤴️回到頂部

3-5 useObservable Hooks和RxJS優雅的結合(rxjs-hooks)

若是要配合RxJS使用,LeetCode團隊封裝了一個rxjs-hooks庫,用起來則要優雅不少, 很是推薦:

function App() {
  const value = useObservable(() => interval(500).pipe(map((val) => val * 3)));

  return (
    <div className="App">
      <h1>Incremental number: {value}</h1>
    </div>
  );
}
複製代碼

⤴️回到頂部

3-6 useEventEmitter 對接eventEmitter

我在React組件設計實踐總結04 - 組件的思惟這篇文章裏面提過:自定義 hook 和函數組件的代碼結構基本一致, 因此有時候hooks 寫着寫着原來越像組件, 組件寫着寫着越像 hooks. 我以爲能夠認爲組件就是一種特殊的 hook, 只不過它輸出 Virtual DOM

Hooks跟組件同樣,是一個邏輯和狀態的聚合單元。能夠維護本身的狀態、有本身的'生命週期'.

useEventEmitter就是一個典型的例子,能夠獨立地維護和釋放本身的資源:

const functionReturnObject = () => ({})
const functionReturnArray = () => []

export function useEventEmitter(emmiter: EventEmitter) {
  const disposers = useRef<Function[]>([])
  const listeners = useRef<{ [name: string]: Function }>({})

  const on = useCallback(<P>(name: string, cb: (data: P) => void) => {
    if (!(name in listeners.current)) {
      const call = (...args: any[]) => {
        const fn = listeners.current[name]
        if (fn) {
          fn(...args)
        }
      }
      // 監聽eventEmitter
      emmiter.on(name, call)
      disposers.current.push(() => {
        emmiter.off(name, call)
      })
    }

    listeners.current[name] = cb
  }, [])

  useEffect(() => {
    // 資源釋放
    return () => {
      disposers.current.forEach(i => i())
    }
  }, [])

  return {
    on,
    emit: emmiter.emit,
  }
}


// ---------
// EXAMPLE
// ---------
function Demo() {
  const { on, emit } = useEventEmitter(eventBus)

  // 事件監聽
  on('someEvent', () => {
    // do something
  })

  const handleClick = useCallback(() => {
    // 事件觸發
    emit('anotherEvent', someData)
  }, [])

  return (<div onClick={handleClick}>...</div>)
}
複製代碼


更多腦洞:


⤴️回到頂部

4. Context的妙用

經過useContext能夠方便地引用Context。不過須要注意的是若是上級Context.Provider的value變化,使用useContext的組件就會被強制從新渲染。

4-1 useTheme 主題配置

本來須要使用高階組件注入或Context.Consumer獲取的Context值,如今變得很是簡潔:

/**
 * 傳統方式
 */
// 經過高階組件注入
withTheme(MyComponent)

// 獲取利用Context.Consumer
const MyComponentWithTheme = (props) => {
  return (<ThemeContext.Consumer>
    {value => <MyComponent theme={value} {...props}></MyComponent>}
  </ThemeContext.Consumer>)
}
複製代碼

Hooks方式

import React, { useContext, FC } from 'react'

const ThemeContext = React.createContext<object>({})

export const ThemeProvider: FC<{ theme: object }> = props => {
  return <ThemeContext.Provider value={props.theme}>{props.children}</ThemeContext.Provider>
}

export function useTheme<T extends object>(): T {
  return useContext(ThemeContext)
}

// ---------
// EXAMPLE
// ---------
const theme = {
  primary: '#000',
  secondary: '#444',
}

function App() {
  return (
    <ThemeProvider theme={theme}>
      <div>...</div>
    </ThemeProvider>
  )
}

const Button: FC = props => {
  const t = useTheme<typeof theme>()
  const style = {
    color: t.primary,
  }
  return <button style={style}>{props.children}</button>
}
複製代碼

⤴️回到頂部

4-2 unstated 簡單狀態管理器

Hooks + Context 也能夠用於實現簡單的狀態管理。

我在React組件設計實踐總結05 - 狀態管理就提到過unstated-next, 這個庫只有主體代碼十幾行,利用了React自己的機制來實現狀態管理.

先來看看使用示例

import React, { useState } from "react"
import { createContainer } from "unstated-next"

function useCounter(initialState = 0) {
  let [count, setCount] = useState(initialState)
  let decrement = () => setCount(count - 1)
  let increment = () => setCount(count + 1)
  return { count, decrement, increment }
}

let Counter = createContainer(useCounter)

function CounterDisplay() {
  let counter = Counter.useContainer()
  return (
    <div>
      <button onClick={counter.decrement}>-</button>
      <span>{counter.count}</span>
      <button onClick={counter.increment}>+</button>
    </div>
  )
}
複製代碼

看看它的源碼:

export function createContainer(useHook) {
  // 只是建立一個Context
	let Context = React.createContext(null)

	function Provider(props) {
		let value = useHook(props.initialState)
		return <Context.Provider value={value}>{props.children}</Context.Provider>
	}

	function useContainer() {
    // 只是使用useContext
		let value = React.useContext(Context)
		if (value === null) {
			throw new Error("Component must be wrapped with <Container.Provider>")
		}
		return value
	}

	return { Provider, useContainer }
}

export function useContainer(container) {
	return container.useContainer()
}
複製代碼

到這裏,你會說,我靠,就這樣? 這個庫感受啥事情都沒幹啊?

須要注意的是, Context不是萬金油,它做爲狀態管理有一個比較致命的缺陷,我在淺談React性能優化的方向文章中也提到了這一點: 它是能夠穿透React.memo或者shouldComponentUpdate的比對的,也就是說,一旦 Context 的 Value 變更,全部依賴該 Context 的組件會所有 forceUpdate

因此若是你打算使用Context做爲狀態管理,必定要注意規避這一點. 它可能會致使組件頻繁從新渲染.


其餘狀態管理方案:


⤴️回到頂部

4-3 useI18n 國際化

I18n是另外一個Context的典型使用場景。react-intlreact-i18next都與時俱進,推出了本身的Hook API, 基本上本來使用高階組件(HOC)實現的功能均可以用Hooks代替,讓代碼變得更加簡潔:

import React from 'react';
import { useTranslation } from 'react-i18next';

export function MyComponent() {
  const { t, i18n } = useTranslation();

  return <p>{t('my translated text')}</p>
}
複製代碼

⤴️回到頂部

4-4 useRouter 簡化路由狀態的訪問

React Hooks 推出已經接近一年,ReactRouter居然尚未正式推出Hook API。不過它們也提上了計劃 —— The Future of React Router and @reach/router,5.X版本會推出Hook API. 咱們暫時先看看一些代碼示例:

function SomeComponent() {
  // 訪問路由變量
  const { userId } = useParams()
  // ...
}

function usePageViews() {
  // 訪問location對象
  // 本來對於非路由組件,須要訪問路由信息須要經過withRouter高階組件注入
  const { location } = useLocation()
  useEffect(() => {
    ga('send', 'pageview', location.pathname)
  }, [location])
}
複製代碼

再等等吧!


⤴️回到頂部

4-5 react-hook-form Hooks和表單能擦出什麼火花?

react-hook-form是Hooks+Form的典型案例,比較符合我理想中的表單管理方式:

import React from 'react';
import useForm from 'react-hook-form';

function App() {
  const { register, handleSubmit, errors } = useForm(); // initialise the hook
  const onSubmit = data => {
    console.log(data);
  }; // callback when validation pass

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstname" ref={register} /> {/* register an input */}
      
      <input name="lastname" ref={register({ required: true })} />
      {errors.lastname && 'Last name is required.'}
      
      <input name="age" ref={register({ pattern: /\d+/ })} />
      {errors.age && 'Please enter number for age.'}
      
      <input type="submit" />
    </form>
  );
}
複製代碼

⤴️回到頂部


5. 反作用封裝

咱們能夠利用Hooks來封裝或監聽組件外部的反作用,將它們轉換爲組件的狀態


5-1 useTimeout 超時修改狀態

useTimeout由用戶觸發,在指定時間後恢復狀態. 好比能夠用於'短時間禁用'按鈕, 避免重複點擊:

function Demo() {
  const [disabled, start] = useTimeout(5000)
  const handleClick = () => {
    start()
    dosomething()
  }

  return <Button onClick={handleClick} disabled={disabled}>點我</Button>
}
複製代碼

實現:

function useTimeout(ms: string) {
  const [ready, setReady] = useState(false)
  const timerRef = useRef<number>()

  const start = useCallback(() => {
    clearTimeout(timerRef.current)
    setReady(true)
    timerRef.current = setTimeout(() => {
      setReady(false)
    }, ms)
  }, [ms])

  const stop = useCallback(() => {
    clearTimeout(timeRef.current)
  }, [])

  useOnUnmount(stop)

  return [ready, start, stop]
}
複製代碼

⤴️回到頂部

5-2 useOnlineStatus 監聽在線狀態

反作用封裝一個比較典型的案例就是監聽主機的在線狀態:

function getOnlineStatus() {
  return typeof navigator.onLine === 'boolean' ? navigator.onLine : true
}

function useOnlineStatus() {
  let [onlineStatus, setOnlineStatus] = useState(getOnlineStatus())

  useEffect(() => {
    const online = () => setOnlineStatus(true)
    const offline = () => setOnlineStatus(false)
    window.addEventListener('online', online)
    window.addEventListener('offline', offline)

    return () => {
      window.removeEventListener('online', online)
      window.removeEventListener('offline', offline)
    }
  }, [])

  return onlineStatus
}

// --------
// EXAMPLE
// --------
function Demo() {
  let onlineStatus = useOnlineStatus();
  return (
    <div>
      <h1>網絡狀態: {onlineStatus ? "在線" : "離線"}</h1>
    </div>
  );
}
複製代碼

還有不少案例, 這裏就不一一列舉,讀者能夠本身嘗試去實現,好比:

  • useDeviceOrientation 監聽設備方向
  • useGeolocation 監聽GPS座標變化
  • useScrollPosition 監聽滾動位置
  • useMotion 監聽設備運動
  • useMediaDevice 監聽媒體設備
  • useDarkMode 夜間模式監聽
  • useKeyBindings 監聽快捷鍵
  • ....

⤴️回到頂部


6. 反作用衍生

反作用封裝相反,反作用衍生是指當組件狀態變化時,衍生出其餘反作用. 二者的方向是相反的.

反作用衍生主要會用到useEffect,使用useEffect來響應狀態的變化.

6-1 useTitle 設置文檔title

useTitle是最簡單的,當給定的值變化時,更新document.title

function useTitle(t: string) {
  useEffect(() => {
    document.title = t
  }, [t])
}

// --------
// EXAMPLE
// --------
function Demo(props) {
  useTitle(props.isEdit ? '編輯' : '新增')
  // ....
}
複製代碼

⤴️回到頂部

6-2 useDebounce

再來個複雜一點的,useDebounce:當某些狀態變化時,它會延遲執行某些操做:

function useDebounce(fn: () => void, args?: any[], ms: number = 100, skipMount?: boolean) {
  const mounted = useRef(false)
  useEffect(() => {
    // 跳過掛載執行
    if (skipMount && !mounted.current) {
      mounted.current = true
      return undefined
    }

    const timer = setTimeout(fn, ms)

    return () => {
      // 若是args變化,先清除計時器
      clearTimeout(timer)
    }
  }, args)
}

// -----------
// EXAMPLE
// -----------
const returnEmptyArray = () => []
function Demo() {
  const [query, setQuery] = useState('')
  const [list, setList] = useState(returnEmptyArray)

  // 搜索
  const handleSearch = async () => {
    setList(await fetchList(query))
  }

  // 當query變化時執行搜索
  useDebounce(handleSearch, [query], 500)

  return (<div>
    <SearchBar value={query} onChange={setQuery} />
    <Result list={list}></Result>
  </div>)
}
複製代碼

⤴️回到頂部

6-3 useThrottle

同理能夠實現useThrottle, 下面的例子來源於react-use:

const useThrottleFn = <T>(fn: (...args: any[]) => T, ms: number = 200, args: any[]) => {
  const [state, setState] = useState<T>(null as any);
  const timeout = useRef<any>(null);
  const nextArgs = useRef(null) as any;
  const hasNextArgs = useRef(false) as any;

  useEffect(() => {
    if (!timeout.current) {
      setState(fn(...args));
      const timeoutCallback = () => {
        if (hasNextArgs.current) {
          hasNextArgs.current = false;
          setState(fn(...nextArgs.current));
          timeout.current = setTimeout(timeoutCallback, ms);
        } else {
          timeout.current = null;
        }
      };
      timeout.current = setTimeout(timeoutCallback, ms);
    } else {
      nextArgs.current = args;
      hasNextArgs.current = true;
    }
  }, args);

  useOnUnmount(() => {
    clearTimeout(timeout.current);
  });

  return state;
};
複製代碼

⤴️回到頂部


7. 簡化業務邏輯

80%的程序員80%的時間在寫業務代碼. 有了Hooks,React開發者如獲至寶. 組件的代碼能夠變得很精簡,且這些Hooks能夠方便地在組件之間複用:


下面介紹,如何利用Hooks來簡化業務代碼

7-1 usePromise 封裝異步請求

第一個例子,試試封裝一下promise,簡化簡單頁面異步請求的流程. 先來看看usePromise的使用示例,我理想中的usePromise應該長這樣:

function Demo() {
  const list = usePromise(async (id: string) => {
    return fetchList(id)
  })

  return (<div>
    {/* 觸發請求 */}
    <button onClick={() => list.callIgnoreError('myId')}>Get List</button>
    {/* 錯誤信息展現和重試 */}
    {!!list.error && <ErrorMessage error={list.error} retry={list.retry}>加載失敗:</ErrorMessage>}
    {/* 加載狀態 */}
    <Loader loading={list.loading}>
      {/* 請求結果 */}
      <Result value={list.value}></Result>
    </Loader>
  </div>)
}
複製代碼

usePromise是我用得比較多的一個Hooks,因此我把它完整的代碼,包括Typescript註解都貼出來,供你們參考參考:

// 定義usePromise的返回值
export interface Res<T, S> {
  loading: boolean
  error?: Error
  value?: S
  setValue: (v: S) => void
  call: T
  callIgnoreError: T
  reset: () => void
  retry: () => void
}

// 定義usePromise 參數
export interface UsePromiseOptions {
  // 若是promise正在加載中則跳過,默認爲true
  skipOnLoading?: boolean
}

// 👇 下面是一堆Typescript函數重載聲明,爲了方便Typescript推斷泛型變量. 小白能夠跳過
function usePromise<T>(action: () => Promise<T>, option?: UsePromiseOptions): Res<() => Promise<T>, T>
function usePromise<T, A>(action: (arg0: A) => Promise<T>, option?: UsePromiseOptions): Res<(arg0: A) => Promise<T>, T>
function usePromise<T, A, B>(action: (arg0: A, arg1: B) => Promise<T>, option?: UsePromiseOptions): Res<(arg0: A, arg1: B) => Promise<T>, T>
function usePromise<T, A, B, C>( action: (arg0: A, arg1: B, arg2: C) => Promise<T>, option?: UsePromiseOptions): Res<(arg0: A, arg1: B, arg2: C) => Promise<T>, T>
function usePromise<T, A, B, C, D>(action: (arg0: A, arg1: B, arg2: C, arg3: D) => Promise<T>, option?: UsePromiseOptions): Res<(arg0: A, arg1: B, arg2: C, arg3: D) => Promise<T>, T>
function usePromise(action: (...args: any[]) => Promise<any>, option?: UsePromiseOptions): Res<(...args: any) => Promise<any>, any>
// 👆 上面是一堆Typescript函數重載聲明,能夠跳過

/**
 * 接受一個action,用於執行異步操做
 */
function usePromise(
  action: (...args: any[]) => Promise<any>,
  option: UsePromiseOptions = { skipOnLoading: true },
): Res<(...args: any) => Promise<any>, any> {
  const actionRef = useRefProps(action)
  const optionRef = useRefProps(option)
  const [loading, setLoading, loadingRef] = useRefState(false)
  const taskIdRef = useRef<number>()
  const argsRef = useRef<any[]>()
  const [value, setValue] = useState()
  const [error, setError, errorRef] = useRefState<Error | undefined>()

  const caller = useCallback(async (...args: any[]) => {
    argsRef.current = args
    if (loadingRef.current && optionRef.current.skipOnLoading) {
      return
    }
    const taskId = getUid()
    taskIdRef.current = taskId

    // 已經有新的任務在執行了,什麼都不作
    const shouldContinue = () => {
      if (taskId !== taskIdRef.current) {
        return false
      }
      return true
    }

    try {
      setLoading(true)
      setError(undefined)
      const res = await actionRef.current(...args)

      if (!shouldContinue()) return
      setValue(res)
      return res
    } catch (err) {
      if (shouldContinue()) {
        setError(err)
      }
      throw err
    } finally {
      if (shouldContinue()) {
        setLoading(false)
      }
    }
  }, [])

  // 不拋出異常
  const callIgnoreError = useCallback(
    async (...args: any[]) => {
      try {
        return await caller(...args)
      } catch {
        // ignore
      }
    },
    [caller],
  )

  const reset = useCallback(() => {
    setLoading(false)
    setValue(undefined)
    setError(undefined)
  }, [])

  // 失敗後重試
  const retry = useCallback(() => {
    if (argsRef.current && errorRef.current) {
      return callIgnoreError(...argsRef.current)
    }
    throw new Error(`not call yet`)
  }, [])

  return {
    loading,
    error,
    call: caller,
    callIgnoreError,
    value,
    setValue,
    reset,
    retry,
  }
}
複製代碼

⤴️回到頂部

7-2 usePromiseEffect 自動進行異步請求

不少時候,咱們是在組件一掛載或者某些狀態變化時自動進行一步請求的,咱們在usePromise的基礎上,結合useEffect來實現自動調用:

// 爲了縮短篇幅,這裏就不考慮跟usePromise同樣的函數重載了
export default function usePromiseEffect<T>(
  action: (...args: any[]) => Promise<T>,
  args?: any[],
) {
  const prom = usePromise(action)

  // 使用useEffect監聽參數變更並執行
  useEffect(() => {
    prom.callIgnoreError.apply(null, args)
  }, args)

  return prom
}

// ---------
// EXAMPLE
// ---------
function Demo(props) {
  // 在掛載或者id變化時請求
  const list = usePromiseEffect((id) => fetchById(id), [id])

  // 同usePromise
}
複製代碼

看到這裏,應該驚歎Hooks的抽象能力了吧!😸


⤴️回到頂部

7-3 useInfiniteList 實現無限加載列表

這裏例子在以前的文章中也說起過

export default function useInfiniteList<T>(
  fn: (params: { offset: number; pageSize: number; list: T[] }) => Promise<T[]>,
  pageSize: number = 20,
) {
  const [list, setList] = useState<T[]>(returnEmptyArray)
  // 列表是否所有加載完畢
  const [hasMore, setHasMore, hasMoreRef] = useRefState(true)
  // 列表是否爲空
  const [empty, setEmpty] = useState(false)
  const promise = usePromise(() => fn({ list, offset: list.length, pageSize }))

  const load = useCallback(async () => {
    if (!hasMoreRef.current) {
      return
    }
    const res = await promise.call()
    if (res.length < pageSize) {
      setHasMore(false)
    }

    setList(l => {
      if (res.length === 0 && l.length === 0) {
        setEmpty(true)
      }

      return [...l, ...res]
    })
  }, [])

  // 清空列表
  const clean = useCallback(() => {
    setList([])
    setHasMore(true)
    setEmpty(false)
    promise.reset()
  }, [])

  // 刷新列表
  const refresh = useCallback(() => {
    clean()
    setTimeout(() => {
      load()
    })
  }, [])

  return {
    list,
    hasMore,
    empty,
    loading: promise.loading,
    error: promise.error,
    load,
    refresh,
  }
}
複製代碼

使用示例:

interface Item {
  id: number
  name: string
}
function App() {
  const { load, list, hasMore, refresh } = useInfiniteList<Item>(async ({ offset, pageSize }) => { const list = [] for (let i = offset; i < offset + pageSize; i++) { if (i === 200) { break } list.push({ id: i, name: `${i}-----` }) } return list }) useEffect(() => { load() }, []) return ( <div className="App"> <button onClick={refresh}>Refresh</button> {list.map(i => ( <div key={i.id}>{i.name}</div> ))} {hasMore ? <button onClick={load}>Load more</button> : <div>No more</div>} </div> ) } 複製代碼

⤴️回到頂部

7-4 usePoll 用hook實現輪詢

下面使用Hooks實現一個定時輪詢器

export interface UsePollOptions<T> {
  /** * 決定是否要繼續輪詢 * @param arg 上一次輪詢返回的值 */
  condition: (arg?: T, error?: Error) => Promise<boolean>
  /** * 輪詢操做 */
  poller: () => Promise<T>
  onError?: (err: Error) => void
  /** * 輪詢間隔. 默認 5000 */
  duration?: number
  /** * 監聽的參數,當這些參數變化時,從新檢查輪詢條件,決定是否繼續輪詢 */
  args?: any[]
  /** * 是否當即輪詢 */
  immediately?: boolean
}

/** * 實現頁面輪詢機制 */
export default function usePoll<T = any>(options: UsePollOptions<T>) {
  const [polling, setPolling, pollingRef] = useRefState(false)
  const [error, setError] = useState<Error>()
  const state = useInstance<{ timer?: number; unmounted?: boolean }>({})
  const optionsRef = useRefProps(options)

  const poll = useCallback(async (immediate?: boolean) => {
    // 已經卸載,或其餘輪詢器正在輪詢
    if (state.unmounted || pollingRef.current) return
    setPolling(true)
    state.timer = window.setTimeout(
      async () => {
        if (state.unmounted) return
        try {
          let res: T | undefined
          let error: Error | undefined
          setError(undefined)

          try {
            res = await optionsRef.current.poller()
          } catch (err) {
            error = err
            setError(err)
            if (optionsRef.current.onError) {
              optionsRef.current.onError(err)
            }
          }
          // 準備下一次輪詢
          if (!state.unmounted && (await optionsRef.current.condition(res, error))) {
            setTimeout(poll)
          }
        } finally {
          !state.unmounted && setPolling(false)
        }
      },
      immediate ? 0 : optionsRef.current.duration || 5000,
    )
  }, [])

  useOnUpdate(
    async () => {
      if (await optionsRef.current.condition()) poll(options.immediately)
    },
    options.args || [],
    false,
  )

  useOnUnmount(() => {
    state.unmounted = true
    clearTimeout(state.timer)
  })

  return { polling, error }
}
複製代碼

使用示例:

function Demo() {
  const [query, setQuery] = useState(')
  const [result, setResult] = useState<Result>()
  usePoll({
    poller: await() => {
      const res =await fetch(query)
      setResult(res)
      return res
    }
    condition: async () => {
      return query !== ''
    },
    args: [query],
  })

  // ...
}
複製代碼

⤴️回到頂部

7-5 業務邏輯抽離

經過上面的案例能夠看到, Hooks很是適合用於抽離重複的業務邏輯。

React組件設計實踐總結02 - 組件的組織介紹了容器組件和展現組件分離,Hooks時代,咱們能夠天然地將邏輯都放置到Hooks中,實現邏輯和視圖的分離

抽離的後業務邏輯能夠複用於不一樣的'展現平臺', 例如 web 版和 native 版:

Login/
  useLogin.ts   // 將全部邏輯都抽取到Hooks中
  index.web.tsx // 只保留視圖
  index.tsx
複製代碼


⤴️回到頂部

8. 開腦洞

一些奇奇怪怪的東西,不知道怎麼分類。做者想象力很是豐富!

8-1 useScript: Hooks + Suspend = ❤️

這個案例來源於the-platform, 使用script標籤來加載外部腳本:

// 注意: 這仍是實驗性特性
import {createResource} from 'react-cache'

export const ScriptResource = createResource((src: string) => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.onload = () => resolve(script);
    script.onerror = reject;
    document.body.appendChild(script);
  });
});


function useScript(options: { src: string }) {
  return ScriptResource.read(src);
}
複製代碼

使用示例:

import { useScript } from 'the-platform';

const Example = () => {
   useScript({ src: 'bundle.js' });
  // ...
};

// Suspend
function App() {
  return <Suspense fallback={'loading...'}><Example></Example></Suspense>
}
複製代碼

同理還能夠實現


⤴️回到頂部

8-2 useModal 模態框數據流管理

我在React組件設計實踐總結04 - 組件的思惟也舉到一個使用Hooks + Context來巧妙實現模態框管理的例子。

先來看看如何使用Context來渲染模態框, 很簡單, ModalContext.Provider給下級組件暴露一個render方法,經過這個方法來傳遞須要渲染的模態框組件和props:

// 模態框組件要實現的接口
export interface BaseModalProps {
  visible: boolean
  onHide: () => void
}

interface ModalContextValue {
  render(Component: React.ComponentType<any>, props: any): void
}

const Context = React.createContext<ModalContextValue>({
  render: () => {
    throw new Error("useModal 必須在ModalRenderer 下級")
  },
})

// 模態框渲染器
const ModalRenderer: FC<{}> = props => {
  const [modal, setModal] = useState<
    | { Comp: React.ComponentType<any>; props: any; visible?: boolean }
    | undefined
  >()

  const hide = useCallback(() => {
    setModal(prev => prev && { ...prev, visible: false })
  }, [])

  // 由下級組件調用,傳遞須要渲染的組件和props
  const render = useCallback<ModalContextValue["render"]>((Comp, props) => { setModal({ Comp, props, visible: true }) }, []) const value = useMemo(() => ({render}), []) return ( <Context.Provider value={value}> {props.children} <div className="modal-container"> {/*模態框渲染 */} {!!modal && React.createElement(modal.Comp, { ...modal.props, visible: modal.visible, onHide: hide, })} </div> </Context.Provider> ) } 複製代碼

再看看Hooks的實現, 也很簡單,就是使用useContext來訪問ModalContext, 並調用render方法:

export function useModal<P extends BaseModalProps>( Modal: React.ComponentType<P>, ) {
  const renderer = useContext(Context)

  return useCallback(
    (props: Omit<P, keyof BaseModalProps>) => {
      renderer.render(Modal, props || {})
    },
    [Modal],
  )
}
複製代碼

應用示例:

const MyModal: FC<BaseModalProps & { a: number }> = props => {
  return (
    <Modal visible={props.visible} onOk={props.onHide} onCancel={props.onHide}>
      {props.a}
    </Modal>
  )
}

const Home: FC<{}> = props => {
  const showMyModal = useModal(MyModal)

  const handleShow = useCallback(() => {
    // 顯示模態框
    showMyModal({
      a: 123,
    })
  }, [])

  return (
    <div>
      showMyModal: <button onClick={handleShow}>show</button>
    </div>
  )
}
複製代碼

可運行的完整示例能夠看這裏


⤴️回到頂部

React Hooks 技術地圖

全家桶和Hooks的結合:


一些有趣的Hooks集合:


Awesome


FAQ



總結

本文篇幅很長、代碼不少。能滑到這裏至關不容易, 給你點個贊。

你用React Hook遇到過什麼問題? 開過什麼腦洞,下方評論告訴我.

歡迎關注我, 和我交流. 我有社恐, 但想多交些圈內朋友(atob('YmxhbmstY2FybmV5'), 備註掘金,我不喝茶,近期也不換工做)

本文完!

相關文章
相關標籤/搜索