React Hooks介紹及應用場景
引言 & 背景
在使用React開發相關頁面或維護相關元件時,常常會遇到一些難以避免的問題,比如:
- 元件中與狀態相關的邏輯很難複用,例如相似彈層元件的開啟、關閉、loading等
- 邏輯複雜的元件 難以開發與維護,當我們的元件需要處理多個互不相關的 local state 時,每個生命週期函式中可能會包含著各種互不相關的邏輯在裡面。
- class元件中的this增加學習成本,class元件在基於現有工具的優化上存在些許問題。
- 由於業務變動,函式元件不得不改為類元件等等。
為了解決這類問題,目前常見的解決方法為render props 與 higher-order components ,但這兩種方式可能會很笨重,而且會導致JSX巢狀過深。
// 普通方法 import React from 'react' import ReactDOM from 'react-dom' const App = React.createClass({ getInitialState() { return { x: 0, y: 0 } }, handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }) }, render() { const { x, y } = this.state return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>The mouse position is ({x}, {y})</h1> </div> ) } }) ReactDOM.render(<App/>, document.getElementById('app')) 複製程式碼
// mixin // 缺點ES6 classes,state修改來源很難追蹤,多個mixin名稱衝突 import React from 'react' import ReactDOM from 'react-dom' // This mixin contains the boilerplate code that // you'd need in any app that tracks the mouse position. // We can put it in a mixin so we can easily share // this code with other components! const MouseMixin = { getInitialState() { return { x: 0, y: 0 } }, handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY }) } } const App = React.createClass({ // Use the mixin! mixins: [ MouseMixin ], render() { const { x, y } = this.state return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <h1>The mouse position is ({x}, {y})</h1> </div> ) } }) ReactDOM.render(<App/>, document.getElementById('app')) 複製程式碼
// HOC // 缺點JSX巢狀,props來源很難確定,多個HOC導致props衝突以及props傳遞問題 const withMouse = (Component) => { return class extends React.Component { state = { x: 0, y: 0 } handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> <Component {...this.props} mouse={this.state}/> </div> ) } } } const App = React.createClass({ render() { // Instead of maintaining our own state, // we get the mouse position as a prop! const { x, y } = this.props.mouse return ( <div style={{ height: '100%' }}> <h1>The mouse position is ({x}, {y})</h1> </div> ) } }) // Just wrap your component in withMouse and // it'll get the mouse prop! const AppWithMouse = withMouse(App) 複製程式碼
import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' // Instead of using a HOC, we can share code using a // regular component with a render prop! class Mouse extends React.Component { static propTypes = { render: PropTypes.func.isRequired } state = { x: 0, y: 0 } handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}> {this.props.render(this.state)} </div> ) } } const App = React.createClass({ render() { return ( <div style={{ height: '100%' }}> <Mouse render={({ x, y }) => ( // The render prop gives us the state we need // to render whatever we want here. <h1>The mouse position is ({x}, {y})</h1> )}/> </div> ) } }) ReactDOM.render(<App/>, document.getElementById('app')) // render props -> hoc const withMouse = (Component) => { return class extends React.Component { render() { return <Mouse render={mouse => ( <Component {...this.props} mouse={mouse}/> )}/> } } } 複製程式碼
React Hooks概述
基於上述原因,react團隊在React 16.7.0-alpha
推出了React Hooks這一新的特性。
首先用一段簡單的程式碼介紹一下什麼是React Hooks
import { useState, useEffect } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } 複製程式碼
什麼是React Hooks? React Hooks是一種React中使用的特殊的函式,例如**useState**可以使我們在React函式元件中使用state,**useEffect**可以使我們在函式式元件中處理副作用。
React Hooks 帶來的好處不僅是 “更 FP,更新粒度更細,程式碼更清晰”,還有如下三個特性:
render
React Hooks生命週期
useState 什麼時候執行? 它會在元件每次render的時候呼叫
useEffect 什麼時候執行? 它會在元件 mount 和 unmount 以及每次重新渲染的時候都會執行,也就是會在 componentDidMount、componentDidUpdate、componentWillUnmount 這三個時期執行。
清理函式(clean up)什麼時候執行? 它會在前一次 effect執行後,下一次 effect 將要執行前,以及 Unmount 時期執行
React Hooks 組合與自定義Hooks
import { useState, useEffect } from "react"; // 底層 Hooks, 返回布林值:是否線上 function useFriendStatusBoolean(friendID) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } // 上層 Hooks,根據線上狀態返回字串:Loading... or Online or Offline function useFriendStatusString(props) { const isOnline = useFriendStatusBoolean(props.friend.id); if (isOnline === null) { return "Loading..."; } return isOnline ? "Online" : "Offline"; } // 使用了底層 Hooks 的 UI function FriendListItem(props) { const isOnline = useFriendStatusBoolean(props.friend.id); return ( <li style={{ color: isOnline ? "green" : "black" }}>{props.friend.name}</li> ); } // 使用了上層 Hooks 的 UI function FriendListStatus(props) { const statu = useFriendStatusString(props.friend.id); return <li>{statu}</li>; } 複製程式碼
React Hooks 應用舉例
利用 useState 建立 Redux
// 這就是 Redux function useReducer(reducer, initialState) { const [state, setState] = useState(initialState); function dispatch(action) { const nextState = reducer(state, action); setState(nextState); } return [state, dispatch]; } // 一個 Action function useTodos() { const [todos, dispatch] = useReducer(todosReducer, []); function handleAddClick(text) { dispatch({ type: "add", text }); } return [todos, { handleAddClick }]; } // 繫結 Todos 的 UI function TodosUI() { const [todos, actions] = useTodos(); return ( <> {todos.map((todo, index) => ( <div>{todo.text}</div> ))} <button onClick={actions.handleAddClick}>Add Todo</button> </> ); } 複製程式碼
利用 hooks取代生命週期
// react呼叫g2 class Component extends React.PureComponent<Props, State> { private chart: G2.Chart = null; private rootDomRef: React.ReactInstance = null; componentDidMount() { this.rootDom = ReactDOM.findDOMNode(this.rootDomRef) as HTMLDivElement; this.chart = new G2.Chart({ container: document.getElementById("chart"), forceFit: true, height: 300 }); this.freshChart(this.props); } componentWillReceiveProps(nextProps: Props) { this.freshChart(nextProps); } componentWillUnmount() { this.chart.destroy(); } freshChart(props: Props) { // do something this.chart.render(); } render() { return <div ref={ref => (this.rootDomRef = ref)} />; } } // hooks呼叫g2 function App() { const ref = React.useRef(null); let chart: G2.Chart = null; React.useEffect(() => { if (!chart) { chart = new G2.Chart({ container: ReactDOM.findDOMNode(ref.current) as HTMLDivElement, width: 500, height: 500 }); } // do something chart.render(); return () => chart.destroy(); }); return <div ref={ref} />; } 複製程式碼
修改頁面 title
function useDocumentTitle(title) { useEffect( () => { document.title = title; return () => (document.title = "大寶"); }, [title] ); } // 使用 useDocumentTitle("包裹複核"); 複製程式碼
監聽頁面大小變化
function getSize() { return { innerHeight: window.innerHeight, innerWidth: window.innerWidth, outerHeight: window.outerHeight, outerWidth: window.outerWidth }; } function useWindowSize() { let [windowSize, setWindowSize] = useState(getSize()); function handleResize() { setWindowSize(getSize()); } useEffect(() => { window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); return windowSize; } // 使用 const windowSize = useWindowSize(); return <div>頁面高度:{windowSize.innerWidth}</div>; 複製程式碼
其他
- DOM 副作用修改 / 監聽。
- React元件輔助函式。
- 處理動畫相關邏輯。
- 處理髮送請求。
- 處理表單填寫。
- 存資料。
注意
- 只能在頂層程式碼(Top Level)中呼叫 Hook
- 不要在迴圈,條件判斷,巢狀函式裡面呼叫 Hooks
- 只在 React 的函式裡面呼叫 Hooks
- 命名時使用use*命名Hooks
- 使用 eslint-plugin-react-hooks 外掛進行檢查
參考文件
- React官方文件
- 精讀《React Hooks》
- 精讀《怎麼用 React Hooks 造輪子》
- 10分鐘瞭解 react 引入的 Hooks
- [ 譯] React hooks: 不是魔法,只是陣列
- Use a Render Prop!
碼字不易,如有建議請掃碼