簡介
React 16.8 於 2019.2 正式發佈,這是一個能提升代碼質量和開發效率的特性,筆者就拋磚引玉先列出一些實踐點,希望得到大家進一步討論。
然而需要理解的是,沒有一個完美的最佳實踐規範,對一個高效團隊來說,穩定的規範比合理的規範更重要,因此這套方案只是最佳實踐之一。
精讀
環境要求
- 擁有較爲穩定且理解函數式編程的前端團隊。
- 開啓 ESLint 插件:eslint-plugin-react-hooks。
組件定義
Function Component 採用 const
+ 箭頭函數方式定義:
const App: React.FC<{ title: string }> = ({ title }) => {
return React.useMemo(() => <div>{title}</div>, [title]);
};
App.defaultProps = {
title: 'Function Component'
}
上面的例子包含了:
- 用
React.FC
申明 Function Component 組件類型與定義 Props 參數類型。 - 用
React.useMemo
優化渲染性能。 - 用
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])
};
結合項目數據流
debounce 優化
比如當輸入框頻繁輸入時,爲了保證頁面流暢,我們會選擇在 onChange
時進行 debounce
。然而在 Function Component 領域中,我們有更優雅的方式實現。
其實在 Input 組件onChange
使用debounce
有一個問題,就是當 Input 組件 受控 時,debounce
的值不能及時回填,導致甚至無法輸入的問題。
我們站在 Function Component 思維模式下思考這個問題:
- React scheduling 通過智能調度系統優化渲染優先級,我們其實不用擔心頻繁變更狀態會導致性能問題。
- 如果聯動一個文本還覺得慢嗎?
onChange
本不慢,大部分使用值的組件也不慢,沒有必要從onChange
源頭開始就debounce
。 - 找到渲染性能最慢的組件(比如 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
時要注意調試上下文,注意父級傳遞的參數引用是否正確,如果引用傳遞不正確,有兩種做法:
- 使用 useDeepCompareEffect 對依賴進行深比較。
- 使用
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 許可證)