React Hooks 系列

一、場景

先理解什麼是hook,官網給的是:javascript

Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。css

二、hooks的意義

  • 在組件之間複用狀態邏輯很難:React 沒有提供將可複用性行爲「附加」到組件的途徑(例如,把組件鏈接到 store),若是你使用過 React 一段時間,你也許會熟悉一些解決此類問題的方案,好比 render props 和 高階組件。可是這類方案須要從新組織你的組件結構,這可能會很麻煩,使你的代碼難以理解。React 須要爲共享狀態邏輯提供更好的原生途徑。
  • 複雜組件變得難以理解:咱們常常維護一些組件,組件起初很簡單,可是逐漸會被狀態邏輯和反作用充斥。每一個生命週期經常包含一些不相關的邏輯
  • 難以理解的 class: JavaScript 中 this 的工做方式
  • 面向生命週期編程變成了面向業務邏輯編程
  • 與時俱進,組件預編譯...
  • 漸進策略,Hooks 和現有代碼能夠同時工做

hooks經過function抽離的方式,實現了複雜邏輯的內部封裝:java

  • 邏輯代碼的複用
  • 減少了代碼體積
  • 沒有this的煩惱

三、舉個例子

規則:react

  • 只能在函數最外層調用 Hook。不要在循環、條件判斷或者子函數中調用。
  • 只能在 React 的函數組件中調用 Hook。不要在其餘 JavaScript 函數中調用。

(1) useState

// class組件
import React from "react";
import "./styles.css";

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
// hooks
import React, { useState } from 'react';

function Example() {
  // 聲明一個叫 "count" 的 state 變量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • 調用 useState 方法的時候作了什麼?定義一個 「state 變量」
  • useState 須要哪些參數?惟一的參數就是初始 state
  • useState 方法的返回值是什麼?當前 state 以及更新 state 的函數

讀取 Stateajax

//class
<p>You clicked {this.state.count} times。</p>
// hook
<p>You clicked {count} times</p>

更新 State算法

//class
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
  Click me
</button>
// hook
<button onClick={() => setCount(count + 1)}>
  Click me
</button>
  • useState的初始值是惰性的,只會在初次渲染組件的時候起做用。
  • useState的更新是替換,而不是合併。
  • useState能夠接收函數參數,並將函數的返回值做爲初始值(便於複雜計算)編程

    const [count, setCount] = useState(() => {
      return Math.random() * 10;
    });
  • useState的更新函數能夠接收函數做爲參數,函數的參數是前一狀態的state值。api

    setCount((count)=> count + 1)
  • 使用當前的值,對state進行更新不會觸發渲染。數組

    const [state, setState] = useState(0);
    // ...
    // 更新 state 不會觸發組件從新渲染,(使用Object.is比較)
    setState(0);
    setState(0);

(2) useEffect

  • useEffect可讓咱們在函數組件中執行反作用操做。事件綁定,數據請求,動態修改DOM。
  • useEffect將會在每一次React渲染以後執行。不管是初次掛載時,仍是更新。(固然這種行爲咱們能夠控制)
  • 若是你熟悉 React class 的生命週期函數,你能夠把 useEffect Hook 看作 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合
  • 在class組件中,一般在componentDidMount中添加對事件的監聽。在componentWillUnmount中會清除對事件的監聽。咱們須要在不一樣的生命週期函數中,拆分咱們的邏輯。
  • 而effect能夠返回一個函數,當react進行清除時, 會執行這個返回的函數。每當執行本次的effect時,都會對上一個effect進行清除。組件卸載時也會執行進行清除。

(2.1)無需清除的 effect

// class組件
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}
// hook
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
  • useEffect 作了什麼? 經過使用這個 Hook,你能夠告訴 React 組件須要在渲染後執行某些操做
  • 爲何在組件內部調用 useEffect? 在 effect 中直接訪問 state 變量
  • useEffect 會在每次渲染後都執行嗎?在第一次渲染以後和每次更新以後都會執行

(2.2)須要清除的 effect

  • 防止引發內存泄露
class FriendStatus extends React.Component {
    constructor(props) {
      super(props);
      this.state = { isOnline: null };
      this.handleStatusChange = this.handleStatusChange.bind(this);
    }

    componentDidMount() {
      ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }
    componentWillUnmount() {
      ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
      );
    }
    handleStatusChange(status) {
      this.setState({
        isOnline: status.isOnline
      });
    }

    render() {
      if (this.state.isOnline === null) {
        return 'Loading...';
      }
      return this.state.isOnline ? 'Online' : 'Offline';
    }
  }
import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
  • 爲何要在 effect 中返回一個函數? 這是 effect 可選的清除機制。每一個 effect 均可以返回一個清除函數。如此能夠將添加和移除訂閱的邏輯放在一塊兒。
  • React 什麼時候清除 effect?執行當前 effect 以前對上一個 effect 進行清除

(2.3)使用多個 Effect 實現關注點分離

  • 解決 class 中生命週期函數常常包含不相關的邏輯
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

(2.4)爲何每次更新的時候都要運行 Effect?

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // 取消訂閱以前的 friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // 訂閱新的 friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  //...
  • useEffect 默認就會在調用一個新的 effect 以前對前一個 effect 進行清理
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 運行第一個 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一個 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 運行下一個 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一個 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 運行下一個 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最後一個 effect

(2.5)經過跳過 Effect 進行性能優化

  • 每次執行effect,清除上一次effect可能會形成沒必要要的性能浪費。咱們能夠經過effect的第二個參數,控制effect的執行。 第二個參數是useEffect的依賴,只有當依賴發生變化時,useEffect纔會更新。
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新

(3) useContext

Context 提供了一個無需爲每層組件手動添加 props,就能在組件樹間進行數據傳遞的方法。瀏覽器

在一個典型的 React 應用中,數據是經過 props 屬性自上而下(由父及子)進行傳遞的,但這種作法對於某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程序中許多組件都須要的。Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。

(3.1) 什麼時候使用 Context?

  • Context 設計目的是爲了共享那些對於一個組件樹而言是「全局」的數據,例如當前認證的用戶、主題或首選語言。
class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 組件接受一個額外的「theme」屬性,而後傳遞給 ThemedButton 組件。
  // 若是應用中每個單獨的按鈕都須要知道 theme 的值,這會是件很麻煩的事,
  // 由於必須將這個值層層傳遞全部組件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

使用 context, 咱們能夠避免經過中間元素傳遞 props:

// Context 可讓咱們無須明確地傳遍每個組件,就能將值深刻傳遞進組件樹。
// 爲當前的 theme 建立一個 context(「light」爲默認值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    // 使用一個 Provider 來將當前的 theme 傳遞給如下的組件樹。
    // 不管多深,任何組件都能讀取這個值。
    // 在這個例子中,咱們將 「dark」 做爲當前的值傳遞下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中間的組件不再必指明往下傳遞 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 讀取當前的 theme context。
  // React 會往上找到最近的 theme Provider,而後使用它的值。
  // 在這個例子中,當前的 theme 值爲 「dark」。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}
const ThemeContext = React.createContext("light");
export default function APP() {
  // 使用一個 Provider 來將當前的 theme 傳遞給如下的組件樹。
  // 不管多深,任何組件都能讀取這個值。
  // 在這個例子中,咱們將 「dark」 做爲當前的值傳遞下去。
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 中間的組件不再必指明往下傳遞 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <div theme={theme}>{theme}</div>;
}

useContext接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定。當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值。即便祖先使用 React.memo 或 shouldComponentUpdate,也會在組件自己使用 useContext 時從新渲染。

useContext(MyContext) 只是讓你可以讀取 context 的值以及訂閱 context 的變化。你仍然須要在上層組件樹中使用 <MyContext.Provider> 來爲下層組件提供 context。

(4) useReducer

useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法。(若是你熟悉 Redux 的話,就已經知道它如何工做了。)

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等。而且,使用 useReducer 還能給那些會觸發深更新的組件作性能優化,由於你能夠向子組件傳遞 dispatch 而不是回調函數 。

  • 用 reducer 重寫 useState 一節的計數器示例:
const initialState = {count: 0};

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();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

(4.1)指定初始 state

有兩種不一樣初始化 useReducer state 的方式,你能夠根據使用場景選擇其中的一種。

  • 將初始 state 做爲第二個參數傳入 useReducer 是最簡單的方法
const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}
  );
  • 惰性初始化:你能夠選擇惰性地建立初始 state。爲此,須要將 init 函數做爲 useReducer 的第三個參數傳入,這樣初始 state 將被設置爲 init(initialArg)。這麼作能夠將用於計算 state 的邏輯提取到 reducer 外部,這也爲未來對重置 state 的 action 作處理提供了便利:
function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
  • 跳過 dispatch

若是 Reducer Hook 的返回值與當前 state 相同,React 將跳過子組件的渲染及反作用的執行。(React 使用 Object.is 比較算法 來比較 state。)

(4.2) useReducer&&useContext 簡單的代替 Redux

import React, { useReducer, useContext, useEffect } from "react";

const store = {
    user: null,
    books: null,
    movies: null
};

function reducer(state, action) {
    switch (action.type) {
        case "setUser":
            return { ...state, user: action.user };
        case "setBooks":
            return { ...state, books: action.books };
        case "setMovies":
            return { ...state, movies: action.movies };
        default:
            throw new Error();
    }
}

const Context = React.createContext(null);

function App() {
    const [state, dispatch] = useReducer(reducer, store);

    const api = { state, dispatch };
    return (
        <Context.Provider value={api}>
            <User />
            <hr />
            <Books />
            <Movies />
        </Context.Provider>
    );
}

function User() {
    const { state, dispatch } = useContext(Context);
    useEffect(() => {
        ajax("/user").then(user => {
            dispatch({ type: "setUser", user: user });
        });
    }, []);
    return (
        <div>
            <h1>我的信息</h1>
            <div>name: {state.user ? state.user.name : ""}</div>
        </div>
    );
}

function Books() {
    const { state, dispatch } = useContext(Context);
    useEffect(() => {
        ajax("/books").then(books => {
            dispatch({ type: "setBooks", books: books });
        });
    }, []);
    return (
        <div>
            <h1>個人書籍</h1>
            <ol>
                {state.books ? state.books.map(book => <li key={book.id}>{book.name}</li>) : "加載中"}
            </ol>
        </div>
    );
}

function Movies() {
    const { state, dispatch } = useContext(Context);
    useEffect(() => {
        ajax("/movies").then(movies => {
            dispatch({ type: "setMovies", movies: movies });
        });
    }, []);
    return (
        <div>
            <h1>個人電影</h1>
            <ol>
                {state.movies
                    ? state.movies.map(movie => <li key={movie.id}>{movie.name}</li>)
                    : "加載中"}
            </ol>
        </div>
    );
}

// 幫助函數
// 兩秒鐘後,根據 path 返回一個對象,一定成功不會失敗
function ajax(path) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (path === "/user") {
                resolve({
                    id: 1,
                    name: "Frank"
                });
            } else if (path === "/books") {
                resolve([
                    {
                        id: 1,
                        name: "JavaScript 高級程序設計"
                    },
                    {
                        id: 2,
                        name: "JavaScript 初級程序設計"
                    }
                ]);
            } else if (path === "/movies") {
                resolve([
                    {
                        id: 1,
                        name: "信條"
                    },
                    {
                        id: 2,
                        name: "八佰"
                    }
                ]);
            }
        }, 2000);
    });
}

(5) useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 把「建立」函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算。
  • 傳入 useMemo 的函數會在渲染期間執行。請不要在這個函數內部執行與渲染無關的操做,諸如反作用這類的操做屬於 useEffect 的適用範疇,而不是 useMemo。
  • 若是沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。
  • 須要先講 React.memo
  • React默認有多餘的render
function App() {
    const [n, setN] = useState(0);
    const [m, setM] = useState(0);
    const onClick = () => {
        setN(n + 1);
    };

    return (
        <div className="App">
            <div>
                {/*點擊button會從新執行Child組件*/}
                <button onClick={onClick}>update n {n}</button>
            </div>
            <Child data={m}/>
            {/* <Child2 data={m}/> */}
        </div>
    );
}

function Child(props) {
    console.log("child 執行了");
    console.log('假設這裏有大量代碼')
    return <div>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

將代碼中的 Child 用React.memo(Child) 代替

function App() {
    const [n, setN] = useState(0);
    const [m, setM] = useState(0);
    const onClick = () => {
        setN(n + 1);
    };

    return (
        <div className="App">
            <div>
                {/*點擊button會從新執行Child組件*/}
                <button onClick={onClick}>update n {n}</button>
            </div>
            <Child data={m}/>
        </div>
    );
}

const Child = React.memo(props => {
        console.log("child 執行了");
        return <div>child: {props.data}</div>;
});
  • 可是,有一個bug,添加了監聽函數以後,一秒破功由於 App 運行時,會再次執行 onClickChild,生成新的函數,新舊函數雖然功能同樣,可是地址引用不同!
function App() {
    const [n, setN] = useState(0);
    const [m, setM] = useState(0);
    const onClick = () => {
        setN(n + 1);
    };
    const onClickChild = () => {}
    return (
        <div className="App">
            <div>
                {/*點擊button會從新執行Child組件*/}
                <button onClick={onClick}>update n {n}</button>
            </div>
            {/*可是若是傳了一個引用,則React.memo無效。由於引用是不相等的*/}
            <Child data={m} onClick={onClickChild}/>
        </div>
    );
}

//使用React.memo能夠解決從新執行Child組件的問題
const Child = React.memo(props => {
        console.log("child 執行了");
        return <div onClick={props.onClick}>child: {props.data}</div>;
});
  • 因此,useMemo
import React, { useState, useMemo } from "react";
import "./styles.css";

export default function App() {
  const [n, setN] = useState(0);
  const [m, setM] = useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClick1 = () => {
    setM(m + 1);
  };
  // const onClickChild = () => {};
  const onClickChild1 = useMemo(() => {
    return () => {
      console.log(`on click child m: ${m}`);
    };
  }, [m]);
  return (
    <div className="App">
      <div>
        {/*點擊button會從新執行Child組件*/}
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick1}>update m {m}</button>
      </div>
      {/*可是若是傳了一個引用,則React.memo無效。由於引用是不相等的*/}
      {/*<Child data={m} onClick={onClickChild}/>*/}
      {/*onClickChild1使用useMemo能夠消除此bug*/}
      <Child data={m} onClick={onClickChild1} />
    </div>
  );
}

//使用React.memo能夠解決從新執行Child組件的問題
const Child = React.memo((props) => {
  console.log("child 執行了");
  return <div onClick={props.onClick}>child: {props.data}</div>;
});
  • 第一個參數是 () => value
  • 第二個參數是依賴 [m, n]
  • 只有當依賴變化時,纔會計算出新的 value
  • 若是依賴不變,那麼就重用以前的 value
  • 若是你的 value 是一個函數,因而就有了useCallback

(6) useCallback

useCallback(fn, deps) 至關於 useMemo(() => fn, deps)。

把內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將很是有用。

(7) useRef

const refContainer = useRef(initialValue);
  • useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。
  • 本質上,useRef 就像是能夠在其 .current 屬性中保存一個可變值的「盒子」。
  • ref 這一種訪問 DOM 的主要方式。若是你將 ref 對象以 <div ref={myRef}></div> 形式傳入組件,則不管該節點如何改變,React 都會將 ref 對象的 .current 屬性設置爲相應的 DOM 節點。
  • useRef() 比 ref 屬性更有用。它能夠很方便地保存任何可變值,其相似於在 class 中使用實例字段的方式。由於它建立的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current: ...} 對象的惟一區別是,useRef 會在每次渲染時返回同一個 ref 對象。
  • 當 ref 對象內容發生變化時,useRef 並不會通知你。變動 .current 屬性不會引起組件從新渲染。若是想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則須要使用回調 ref 來實現。
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
  • 另一個使用場景是獲取 previous props 或 previous state:
import React, { useState, useEffect, useRef } from "react";
import "./styles.css";

export default function APP() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  const prevCount = prevCountRef.current;

  useEffect(() => {
    prevCountRef.current = count;
  });

  const onClick = () => {
    setCount(count + 1);
  };

  return (
    <>
      <h1>
        Now: {count}, before: {prevCount}
      </h1>
      <div onClick={onClick}>onClick div</div>
    </>
  );
}
  • 使用useRef來跨越渲染週期存儲數據,並且對它修改也不會引發組件渲染
import React, { useState, useEffect, useMemo, useRef } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const timerID = useRef();

  useEffect(() => {
    timerID.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);

  useEffect(() => {
    if (count > 10) {
      clearInterval(timerID.current);
    }
  });

  return (
    <>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Count: {count}, double: {doubleCount}
      </button>
    </>
  );
}

(8) useLayoutEffect

  • 可使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製以前,useLayoutEffect 內部的更新計劃將被同步刷新。
  • useLayoutEffect 在瀏覽器渲染前執行
  • useEffect 在瀏覽器渲染完成後執行
const [count, setCount] = useState(0);
  
useLayoutEffect(() => {
  if (count === 0) {
    setCount(10 + Math.random()*200);
  }
}, [count]);

return (
    <div onClick={() => setCount(0)}>{count}</div>
);
const [count, setCount] = useState(0);

useEffect(() => {
  if (count === 0) {
    setCount(10 + Math.random()*200);
  }
}, [count]);

return (
    <div onClick={() => setCount(0)}>{count}</div>
);
  • useLayoutEffect 裏的任務最好影響了 Layout
  • 若是正在將代碼從 class 組件遷移到使用 Hook 的函數組件,則須要注意 useLayoutEffect 與 componentDidMount、componentDidUpdate 的調用階段是同樣的。可是,推薦一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect
  • 若是使用服務端渲染,請記住,不管 useLayoutEffect 仍是 useEffect 都沒法在 Javascript 代碼加載完成以前執行。這就是爲何在服務端渲染組件中引入 useLayoutEffect 代碼時會觸發 React 告警。解決這個問題,須要將代碼邏輯移至 useEffect 中(若是首次渲染不須要這段邏輯的狀況下),或是將該組件延遲到客戶端渲染完成後再顯示(若是直到 useLayoutEffect 執行以前 HTML 都顯示錯亂的狀況下)。

(9) 自定義 Hook

經過自定義 Hook,能夠將組件邏輯提取到可重用的函數中。

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
  • 提取自定義 Hook

當咱們想在兩個函數之間共享邏輯時,咱們會把它提取到第三個函數中。而組件和 Hook 都是函數,因此也一樣適用這種方式。

自定義 Hook 是一個函數,其名稱以 「use」 開頭,函數內部能夠調用其餘的 Hook。 例如,下面的 useFriendStatus 是咱們第一個自定義的 Hook:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

四、FAQ

  • Hook 可否覆蓋 class 的全部使用場景?

    目前暫時尚未對應不經常使用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命週期的 Hook 等價寫法

  • 能夠只在更新時運行 effect 嗎?

    可使用一個可變的 ref 手動存儲一個布爾值來表示是首次渲染仍是後續渲染,而後在你的 effect 中檢查這個標識。

相關文章
相關標籤/搜索