精讀《React Hooks 最佳實踐》

簡介

React 16.8 於 2019.2 正式發佈,這是一個能提升代碼質量和開發效率的特性,筆者就拋磚引玉先列出一些實踐點,希望得到大家進一步討論。

然而需要理解的是,沒有一個完美的最佳實踐規範,對一個高效團隊來說,穩定的規範比合理的規範更重要,因此這套方案只是最佳實踐之一。

精讀

環境要求

組件定義

Function Component 採用 const + 箭頭函數方式定義:

const App: React.FC<{ title: string }> = ({ title }) => {
  return React.useMemo(() => <div>{title}</div>, [title]);
};

App.defaultProps = {
  title: 'Function Component'
}

上面的例子包含了:

  1. React.FC 申明 Function Component 組件類型與定義 Props 參數類型。
  2. React.useMemo  優化渲染性能。
  3. App.defaultProps 定義 Props 的默認值。

FAQ

爲什麼不用 React.memo?

推薦使用 React.useMemo 而不是 React.memo,因爲在組件通信時存在 React.useContext 的用法,這種用法會使所有用到的組件重渲染,只有 React.useMemo 能處理這種場景的按需渲染。

沒有性能問題的組件也要使用 useMemo 嗎?

要,考慮未來維護這個組件的時候,隨時可能會通過 useContext 等注入一些數據,這時候誰會想起來添加 useMemo 呢?

爲什麼不用解構方式代替 defaultProps?

雖然解構方式書寫 defaultProps 更優雅,但存在一個硬傷:對於對象類型每次 Rerender 時引用都會變化,這會帶來性能問題,因此不要這麼做。

局部狀態

局部狀態有三種,根據常用程度依次排列: useState useRef useReducer 。

useState

const [hide, setHide] = React.useState(false);
const [name, setName] = React.useState('BI');

狀態函數名要表意,儘量聚集在一起申明,方便查閱。

useRef

const dom = React.useRef(null);

useRef 儘量少用,大量 Mutable 的數據會影響代碼的可維護性。

但對於不需重複初始化的對象推薦使用 useRef 存儲,比如 new G2() 。

useReducer

局部狀態不推薦使用 useReducer ,會導致函數內部狀態過於複雜,難以閱讀。 useReducer 建議在多組件間通信時,結合 useContext 一起使用。

FAQ

可以在函數內直接申明普通常量或普通函數嗎?

不可以,Function Component 每次渲染都會重新執行,常量推薦放到函數外層避免性能問題,函數推薦使用 useCallback 申明。

函數

所有 Function Component 內函數必須用 React.useCallback 包裹,以保證準確性與性能。

const [hide, setHide] = React.useState(false);
  
const handleClick = React.useCallback(() => {
  setHide(isHide => !isHide)
}, [])

useCallback 第二個參數必須寫,eslint-plugin-react-hooks 插件會自動填寫依賴項。

發請求

發請求分爲操作型發請求與渲染型發請求。

操作型發請求

操作型發請求,作爲回調函數:

return React.useMemo(() => {
  return (
    <div onClick={requestService.addList} />
  )
}, [requestService.addList])

渲染型發請求

渲染型發請求在 useAsync 中進行,比如刷新列表頁,獲取基礎信息,或者進行搜索, 都可以抽象爲依賴了某些變量,當這些變量變化時要重新取數

const { loading, error, value } = useAsync(async () => {
  return requestService.freshList(id);
}, [requestService.freshList, id]);

組件間通信

簡單的組件間通信使用透傳 Props 變量的方式,而頻繁組件間通信使用 React.useContext 。

以一個複雜大組件爲例,如果組件內部拆分了很多模塊, 但需要共享很多內部狀態 ,最佳實踐如下:

定義組件內共享狀態 - store.ts

export const StoreContext = React.createContext<{
  state: State;
  dispatch: React.Dispatch<Action>;
}>(null)

export interface State {};

export interface Action { type: 'xxx' } | { type: 'yyy' };

export const initState: State = {};

export const reducer: React.Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

根組件注入共享狀態 - main.ts

import { StoreContext, reducer, initState } from './store'

const AppProvider: React.FC = props => {
  const [state, dispatch] = React.useReducer(reducer, initState);

  return React.useMemo(() => (
    <StoreContext.Provider value={{ state, dispatch }}>
      <App />
    </StoreContext.Provider>
  ), [state, dispatch])
};

任意子組件訪問/修改共享狀態 - child.ts

import { StoreContext } from './store'

const app: React.FC = () => {
  const { state, dispatch } = React.useContext(StoreContext);
  
  return React.useMemo(() => (
    <div>{state.name}</div>
  ), [state.name])
};

如上解決了 多個聯繫緊密組件模塊間便捷共享狀態的問題 ,但有時也會遇到需要共享根組件 Props 的問題,這種不可修改的狀態不適合一併塞到 StoreContext 裏,我們新建一個 PropsContext 注入根組件的 Props:

const PropsContext = React.createContext<Props>(null)

const AppProvider: React.FC<Props> = props => {
  return React.useMemo(() => (
    <PropsContext.Provider value={props}>
      <App />
    </PropsContext.Provider>
  ), [props])
};

結合項目數據流

參考 react-redux hooks

debounce 優化

比如當輸入框頻繁輸入時,爲了保證頁面流暢,我們會選擇在 onChange 時進行 debounce 。然而在 Function Component 領域中,我們有更優雅的方式實現。

其實在 Input 組件 onChange  使用 debounce 有一個問題,就是當 Input 組件 受控 時, debounce 的值不能及時回填,導致甚至無法輸入的問題。

我們站在 Function Component 思維模式下思考這個問題:

  1. React scheduling 通過智能調度系統優化渲染優先級,我們其實不用擔心頻繁變更狀態會導致性能問題。
  2. 如果聯動一個文本還覺得慢嗎? onChange 本不慢,大部分使用值的組件也不慢,沒有必要從 onChange 源頭開始就 debounce 。
  3. 找到渲染性能最慢的組件(比如 iframe 組件),對一些頻繁導致其渲染的入參進行 useDebounce 。

下面是一個性能很差的組件,引用了變化頻繁的 text (這個 text 可能是 onChange 觸發改變的),我們利用 useDebounce 將其變更的頻率慢下來即可:

const App: React.FC = ({ text }) => {
  // 無論 text 變化多快,textDebounce 最多 1 秒修改一次
  const textDebounce = useDebounce(text, 1000)
  
  return useMemo(() => {
    // 使用 textDebounce,但渲染速度很慢的一堆代碼
  }, [textDebounce])
};

使用 textDebounce 替代 text 可以將渲染頻率控制在我們指定的範圍內。

useEffect 注意事項

事實上,useEffect 是最爲怪異的 Hook,也是最難使用的 Hook。比如下面這段代碼:

useEffect(() => {
  props.onChange(props.id)
}, [props.onChange, props.id])

如果 id 變化,則調用 onChange。但如果上層代碼並沒有對 onChange 進行合理的封裝,導致每次刷新引用都會變動,則會產生嚴重後果。我們假設父級代碼是這麼寫的:

class App {
  render() {
    return <Child id={this.state.id} onChange={id => this.setState({ id })} />
  }
}

這樣會導致死循環。雖然看上去 <App> 只是將更新 id 的時機交給了子元素 <Child>,但由於 onChange 函數在每次渲染時都會重新生成,因此引用總是在變化,就會出現一個無限死循環:

onChange -> useEffect 依賴更新 -> props.onChange -> 父級重渲染 -> 新 onChange...

想要阻止這個循環的發生,只要改爲 onChange={this.handleChange} 即可,useEffect 對外部依賴苛刻的要求,只有在整體項目都注意保持正確的引用時才能優雅生效。

然而被調用處代碼怎麼寫並不受我們控制,這就導致了不規範的父元素可能導致 React Hooks 產生死循環。

因此在使用 useEffect 時要注意調試上下文,注意父級傳遞的參數引用是否正確,如果引用傳遞不正確,有兩種做法:

  1. 使用 useDeepCompareEffect 對依賴進行深比較。
  2. 使用 useCurrentValue 對引用總是變化的 props 進行包裝:
function useCurrentValue<T>(value: T): React.RefObject<T> {
  const ref = React.useRef(null);
  ref.current = value;
  return ref;
}

const App: React.FC = ({ onChange }) => {
  const onChangeCurrent = useCurrentValue(onChange)
};

onChangeCurrent 的引用保持不變,但每次都會指向最新的 props.onChange,從而可以規避這個問題。

總結

如果還有補充,歡迎在文末討論。

如需瞭解 Function Component 或 Hooks 基礎用法,可以參考往期精讀:

討論地址是:精讀《React Hooks 最佳實踐》 · Issue #202 · dt-fe/weekly

如果你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

<img width=200 src="https://img.alicdn.com/tfs/TB...;>

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章