精讀《React Hooks 最佳實踐》

簡介

React 16.8 於 2019.2 正式發佈,這是一個能提高代碼質量和開發效率的特性,筆者就拋磚引玉先列出一些實踐點,但願獲得你們進一步討論。前端

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

精讀

環境要求

組件定義

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

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

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

上面的例子包含了:github

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

FAQ

爲何不用 React.memo?

推薦使用 React.useMemo 而不是 React.memo,由於在組件通訊時存在 React.useContext 的用法,這種用法會使全部用到的組件重渲染,只有 React.useMemo 能處理這種場景的按需渲染。typescript

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

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

爲何不用解構方式代替 defaultProps?

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

局部狀態

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

useState

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

狀態函數名要表意,儘可能彙集在一塊兒申明,方便查閱。api

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 許可證
相關文章
相關標籤/搜索