React Hooks教程之基礎篇

wxz在掘金發表於2019-12-10

什麼是Hooks

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

為什麼要用Hooks

程式碼可讀性好,易於維護

1.hooks在function元件中使用,不用維護複雜的生命週期,不用擔心this指向問題

Hooks給Function元件賦能,Function元件也可維護自己的state,不用擔心元件通訊過程中this指向的問題。

2.更好的邏輯複用方式

自定義hook相比目前react常見的程式碼複用方式(高階元件render props)都要簡單易懂,具體可以參照本章自定義hooks章節

提升開發效率

我們來對比一下同一個功能用class元件實現和使用hooks的function元件實現的程式碼差異,

1.Class元件版本

import React from 'react';
class Person extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
          username: "小明"
      };
  }
  
  componentDidMount() {
      console.log('元件掛載後要做的操作')
  }
  
  componentWillUnmount() {
      console.log('元件解除安裝要做的操作')
  }
  
  componentDidUpdate(prevProps, prevState) {
      if(prevState.username !== this.state.username) {
          console.log('元件更新後的操作')
      }
  }
  
  render() {
      return (
        <div>
            <p>歡迎 {state.username}</p>
            <input type="text" placeholder="input a username" onChange={(event) => this.setState({ username: event.target.value)})}></input>
        </div>
      );
  }
}
複製程式碼

2.Hooks版本

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

export const Person = () => {
  const [name, setName] = useState("小明");
  
  useEffect(() => {
      console.log('元件掛載後要做的操作')
      return () => {
        console.log('元件解除安裝要做的操作')
      }
  }, []);
  
  useEffect(() => {
      console.log('元件更新後的操作')
  }, [name]);
  
  return (
    <div>
        <p>歡迎 {name}</p>
        <input type="text" placeholder="input a username" onChange={(event) => setName( event.target.value)}></input>
    </div>
  )
}
複製程式碼

Hooks版本簡化了很多程式碼,熟悉後可以顯著提升開發效率。

怎樣使用Hooks

Hooks基礎API

useState(重點掌握)

1.引數:

  • 常量:元件初始化的時候就會定義
import React, { useState } from 'react';

function Example() {
  // 宣告一個叫 "count" 的 state 變數,初始值為0,後續通過setCount改變它能讓檢視重新渲染
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼
  • 函式:只有開始渲染的時候函式才會執行
// initialState 引數只會在元件的初始渲染中起作用,後續渲染時會被忽略。
// 如果初始 state 需要通過複雜計算獲得,則可以傳入一個函式,在函式中計算並返回初始的 state,
// 此函式只在初始渲染時被呼叫:
const [count, setCount] = useState(() => {
  const initialCount = someExpensiveComputation(props);
  return initialState;
})
複製程式碼

2.返回值

useState返回值時一個長度為2的陣列,陣列第一項為為定義的變數(名稱自己定),第二項時改變第一項的函式(名稱自己定),具體示例可看上述程式碼。

useEffect(重點掌握)

該 Hook 有兩個引數,第一個引數是一個包含命令式、且可能有副作用程式碼的函式,第二個引數是一個陣列,此引數來控制該Effect包裹的函式執不執行,如果第二個引數不傳遞,則該Effect每次元件重新整理都會執行,相當於class元件中的componentDidMount和componentDidupdate生命週期的融合

1.基本使用方法

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

function Example() {
  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>
  );
}
複製程式碼

2.控制函式的執行

和上述程式碼類似,我們給useEffect傳遞第二個引數[count],這樣只有count改變的時候才會執行

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

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

  // 只有count改變時才會執行
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  },[count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼
import React, { useEffect } from 'react';

function Example() {
  // 元件掛載時只執行一次
  useEffect(() => {
    console.log("只執行一次,類似componentDidMount")
  },[]);

  return (
    <div>只執行一次的Effect</div>
  );
}
複製程式碼

3.需要清除的副作用

有一些副作用是需要清除的。例如訂閱外部資料來源。這種情況下,清除工作是非常重要的,可以防止引起記憶體洩露!

示例1(每次渲染都會清除):
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';
}
複製程式碼
示例2(只有元件解除安裝的時候清除):

但我們給第二個引數傳遞一個空陣列的時候,只有元件解除安裝時,Effect才會執行清除操作,此時的useEffect相當於class元件的componentDidMount和compinentWillUnmount的融合。

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';
}
複製程式碼

我們在日常使用的時候要靈活運用,但儘量使用第二個引數來控制函式的執行,這樣能優化效能。

useContext(重要)

該Hook接收一個 context 物件(React.createContext 的返回值)並返回該 context 的當前值。當前的 context 值由上層元件中距離當前元件最近的 <MyContext.Provider> 的 value prop 決定。

1.使用例項:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

// 主題context
const ThemeContext = React.createContext(themes.light);

function App() {
  // 這裡的value值改變,useContext包裹的值也會改變
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  // 上層最近的Provider的value屬性的值
  const theme = useContext(ThemeContext);

  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}
複製程式碼

2.Class元件實現相同的邏輯請參考react官方文件-Context

簡單示例:

// 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(props) {
  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} />;
  }
}
複製程式碼

useReducer(重要)

useState 的替代方案。它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法(和redux用法十分相近)。

const [state, dispatch] = useReducer(reducer, initialArg, init);
複製程式碼

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於之前的 state 等。並且,使用 useReducer 還能給那些會觸發深更新的元件做效能優化,因為你可以向子元件傳遞 dispatch 而不是回撥函式 。

引數:

  • 第一個引數是reducer純函式
  • 第二個引數是初始的state
  • 第三個引數可以修改初始state,將初始 state 設定為 init(initialArg)

1.基本用法

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>
    </>
  );
}
複製程式碼

useCallback(重點掌握)

把內聯回撥函式及依賴項陣列作為引數傳入 useCallback,它將返回該回撥函式的 memoized 版本,該回撥函式僅在某個依賴項改變時才會更新

  • 常見應用場景:父元件向子元件傳遞會回撥函式(但是react官方不推薦這種方式,官方推薦使用useReducer hook,通過傳遞dispatch來避免這種形式,具體原因參考官方解釋
  • 示例:
import React, { useEffect, useState, useCallback } from 'react';
// 子元件
function Son({callback}) {
    renturn (
        <a onClick={()=>callback("小紅")}>點選切換姓名</a>
    )
}
// 父元件
function Parent() {
  const [name,setName] = useState("")
  useEffect(() => {
    console.log("獲取資料並更新state")
    setName("小明")
  },[]);
  const callback = useCallback(name => {
    setName(name);
  }, []);
  return (
    <>
      <Son callback={callback} />;
      name:{name}
    <>
  )
}
複製程式碼

useMemo(重點掌握)

useCallback(fn, deps) 相當於 useMemo(() => fn, deps)。

把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算

如果沒有提供依賴項陣列,useMemo 在每次渲染時都會計算新的值。

你可以把 useMemo 作為效能優化的手段,但不要把它當成語義上的保證!

應用場景:

  • 儲存一次昂貴的計算
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製程式碼
  • 跳過一次子節點的昂貴的重新渲染
function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}
複製程式碼

useRef(重要)

useRef 返回一個可變的 ref 物件,其 current 屬性被初始化為傳入的引數(initialValue)。返回的 ref 物件在元件的整個生命週期內保持不變

const refContainer = useRef(initialValue);
複製程式碼

使用場景:

  • 訪問子元件dom
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>
    </>
  );
}
複製程式碼
  • 儲存例項變數
function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });
  // ...
  return <div>使用useRef儲存例項變數</div>
}
複製程式碼

useImperativeHandle(不常用)

useImperativeHandle(ref, createHandle, [deps])
複製程式碼

useImperativeHandle 可以讓你在使用 ref 時自定義暴露給父元件的例項值。在大多數情況下,應當避免使用 ref 這樣的命令式程式碼。useImperativeHandle 應當與 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
複製程式碼

在本例中,渲染 <FancyInput ref={inputRef} /> 的父元件可以呼叫 inputRef.current.focus()

useLayoutEffect(不常用)

其函式簽名與 useEffect 相同,使用方法一致,但它會在所有的 DOM 變更之後同步呼叫 effect。可以使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步重新整理。

儘可能使用標準的 useEffect 以避免阻塞視覺更新。

  • useEffect與 componentDidMount、componentDidUpdate 不同的是,在瀏覽器完成佈局與繪製之後,傳給 useEffect 的函式會延遲呼叫。
  • useLayoutEffect則與componentDidMount、componentDidUpdate呼叫時機相同。

useDebugValue(不常用)

開發階段除錯時使用,具體用法參考官方文件

Hook進階

自定義Hooks

通過自定義 Hook,可以將抽取多個元件可重用的邏輯,實現邏輯複用。

示例(以下示例出自阮一峰的網路日誌):

const Person = ({ personId }) => {
  const [loading, setLoading] = useState(true);
  const [person, setPerson] = useState({});

  useEffect(() => {
    setLoading(true); 
    fetch(`https://swapi.co/api/people/${personId}/`)
      .then(response => response.json())
      .then(data => {
        setPerson(data);
        setLoading(false);
      });
  }, [personId])

  if (loading === true) {
    return <p>Loading ...</p>
  }

  return <div>
    <p>You're viewing: {person.name}</p>
    <p>Height: {person.height}</p>
    <p>Mass: {person.mass}</p>
  </div>
}
複製程式碼

我們將上述程式碼中獲取person的邏輯抽離出來,方便其他類似的元件呼叫

const usePerson = (personId) => {
  const [loading, setLoading] = useState(true);
  const [person, setPerson] = useState({});
  useEffect(() => {
    setLoading(true);
    fetch(`https://swapi.co/api/people/${personId}/`)
      .then(response => response.json())
      .then(data => {
        setPerson(data);
        setLoading(false);
      });
  }, [personId]);  
  return [loading, person];
};
複製程式碼

上述程式碼中的usePerson就是一個自定義hook,在其餘元件中我們可以這樣使用:

const Person = ({ personId }) => {
  const [loading, person] = usePerson(personId);

  if (loading === true) {
    return <p>Loading ...</p>;
  }

  return (
    <div>
      <p>You're viewing: {person.name}</p>
      <p>Height: {person.height}</p>
      <p>Mass: {person.mass}</p>
    </div>
  );
};
複製程式碼

自己動手實現幾個常用自定義hooks

  • useFetch(簡單版):獲取介面資料
import { useState, useEffect} from 'react';
import fetch from 'fetch';

/**
 * @param {String} url 
 * @param {Object} initState 
 */
const useFetch_0 = (url, initState) => {
  const [isLoading, setIsLoading] = useState(false);
  const [data, setDate] = useState(initState);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () =>{
      setIsLoading(true);
      try {
        const res = await fetch(url);
        setDate(res);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    }
    fetchData();

  }, [url]);

  return [
    data,
    isLoading,
    isError,
  ];
}

export default useFetch_0;
複製程式碼

父頁面使用:const [data,isLoading,isError] = useFetch(url,initState)

  • usePrevious:獲取上一輪的props和state
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

// 使用
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}
複製程式碼

第三方優質自定義Hooks

github目前已經有很多優質自定義hooks,參考地址:github.com/rehooks/awe…

自定義hooks舉例

useDeepCompareEffect

import React from 'react';
import { useDeepCompareEffect } from 'use-deep-compare';

function App({ object, array }) {
  useDeepCompareEffect(() => {
    // do something significant here
    return () => {
      // return to clean up that significant thing
    };
  }, [object, array]);

  return <div>{/* render significant thing */}</div>;
}
複製程式碼

useDeepCompareCallback

import React from 'react';
import { useDeepCompareCallback } from 'use-deep-compare';

function App({ object, array }) {
  const callback = useDeepCompareCallback(() => {
    // do something significant here
  }, [object, array]);

  return <div>{/* render significant thing */}</div>;
}
複製程式碼

useDeepCompareMemo

import React from 'react';
import { useDeepCompareMemo } from 'use-deep-compare';

function App({ object, array }) {
  const memoized = useDeepCompareMemo(() => {
    // do something significant here
  }, [object, array]);

  return <div>{/* render significant thing */}</div>;
}
複製程式碼
import React, { useState } from 'react';
import { useDebounce } from 'use-debounce';

export default function Input() {
  const [text, setText] = useState('Hello');
  const [value] = useDebounce(text, 1000);

  return (
    <div>
      <input
        defaultValue={'Hello'}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <p>Actual value: {text}</p>
      <p>Debounce value: {value}</p>
    </div>
  );
}
複製程式碼
const data = useAsyncMemo(doAPIRequest, [])
複製程式碼

使用Hooks實現Class元件常用生命週期

  • componentDidMount
useEffect(()=>{
    // do something
},[])
複製程式碼
  • componentDidUpdate
useEffect(()=>{
    // do something
})
複製程式碼
  • componentWillUnmount
useEffect(()=>{
    return ()=> {
        // do something
    }
},[])
複製程式碼
function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    // Row 自上次渲染以來發生過改變。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}
複製程式碼
  • shouldComponentUpdate

可以使用useMemo,如果不涉及比較元件內部state,建議使用memo

function Parent({ a, b }) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}
複製程式碼

Hooks常見問題

大部分常見的問題在上述程式碼中都體現了,其餘問題請參考官方文件問題模組

Hooks注意事項

  • 只在最頂層使用 Hook
  • 只在 React 函式中呼叫 Hook
  • 詳細規則請參考官方文件hooks規則

總結

  • useState和useEffect可以覆蓋絕大多數業務場景
  • 複雜的元件使用useReducer代替useState
  • 在useState和useEffect不滿足業務需求的時候,使用useContext,useRef,或者第三方自定義鉤子來解決
  • useMemo和useCallback用來做效能優化,如果不用他倆程式碼應該也能正確執行

參考文獻

相關文章