react之react Hooks

久宇詩發表於2021-11-23

函式元件,沒有 class 元件中的 componentDidMount、componentDidUpdate 等生命週期方法,也沒有 State,但這些可以通過 React Hook 實現。
React Hooks 的意思是,元件儘量寫成純函式,如果需要外部功能和副作用,就用鉤子把外部程式碼"鉤"進來

React Hooks優點

  1. 程式碼可讀性更強:通過 React Hooks 可以將功能程式碼聚合,方便閱讀維護
  2. 元件樹層級變淺:在原本的程式碼中,我們經常使用 HOC/render props 等方式來複用元件的狀態,增強功能等,無疑增加了元件樹層數及渲染,而在 React Hooks 中,這些功能都可以通過強大的自定義的 Hooks 來實現

九種常用的鉤子

  1. useState:儲存元件狀態
  2. useEffect: 處理副作用
  3. useContext: 減少元件層級
  4. useReducer:類似於redux,通訊
  5. useCallback: 記憶函式
  6. useMemo: 記憶元件
  7. useRef: 儲存引用值
  8. useImperativeHandle: 透傳 Ref
  9. useLayoutEffect: 同步執行副作用

1、useState儲存元件狀態

用來代替:state,setState
若使用物件做 State,useState 更新時會直接替換掉它的值,而不像 setState 一樣把更新的欄位合併進物件中。推薦將 State 物件分成多個 State 變數。
類元件案例
在類元件中,我們使用 this.state 來儲存元件狀態,並對其修改觸發元件重新渲染。

import React from "react";
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: "alife"
    };
  }
  render() {
    const { count } = this.state;
    return (
      <div>
        Count: {count}
        <button onClick={() => this.setState({ count: count + 1 })}>+</button>
        <button onClick={() => this.setState({ count: count - 1 })}>-</button>
      </div>
    );
  }
}

函式元件useState

在函式元件中,由於沒有 this 這個黑魔法,React 通過 useState 來幫我們儲存元件的狀態。

  1. 通過傳入 useState 引數後返回一個帶有預設狀態和改變狀態函式的陣列[obj, setObject] (名稱是自定義的可修改[count, setCount]...)。
  2. 通過傳入 新狀態給函式 來改變原本的狀態值。
import React, { useState } from "react";
function App() {
  const [obj, setObject] = useState({
    count: 0,
    name: "alife"
  });
    let [count, setCount] = useState(0)
  return (
    <div className="App">
      Count: {obj.count}
      <button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
      <button onClick={() => setObject({ ...obj, count: obj.count - 1 })}>-</button>
    </div>
  );
}

注意: useState 不幫助你處理狀態,相較於 setState 非覆蓋式更新狀態,useState 覆蓋式更新狀態,需要開發者自己處理邏輯。(程式碼如上)

2、useEffect 處理副作用

用來代替:componentDidMount、componentDidUpdate和componentWillUnmount的組合體。
預設情況下,useEffect會在第一次渲染之後和每次更新之後執行,每次執行useEffect時,DOM 已經更新完畢。
為了控制useEffect的執行時機與次數,可以使用第二個可選引數施加控制。
類元件案例
在例子中,元件每隔一秒更新元件狀態,並且每次觸發更新都會觸發 document.title 的更新(副作用),而在元件解除安裝時修改 document.title(類似於清除)

從例子中可以看到,一些重複的功能開發者需要在 componentDidMount 和 componentDidUpdate 重複編寫,而如果使用 useEffect 則完全不一樣。

import React, { Component } from "react";
class App extends Component {
  state = {
    count: 1
  };

  componentDidMount() {
    const { count } = this.state;
    document.title = "componentDidMount" + count;
    this.timer = setInterval(() => {
      this.setState(({ count }) => ({
        count: count + 1
      }));
    }, 1000);
  }

  componentDidUpdate() {
    const { count } = this.state;
    document.title = "componentDidMount" + count;
  }

  componentWillUnmount() {
    document.title = "componentWillUnmount";
    clearInterval(this.timer);
  }

  render() {
    const { count } = this.state;
    return (
      <div>
        Count:{count}
        <button onClick={() => clearInterval(this.timer)}>clear</button>
      </div>
    );
  }
}

useEffect
引數1:接收一個函式,可以用來做一些副作用比如非同步請求,修改外部引數等行為。
引數2:稱之為dependencies,是一個陣列,如果陣列中的值變化才會觸發 執行useEffect 第一個引數中的函式。返回值(如果有)則在元件銷燬或者呼叫函式前呼叫

    1. 比如第一個 useEffect 中,理解起來就是一旦 count 值發生改變,則修改 documen.title 值;
    1. 而第二個 useEffect 中傳遞了一個空陣列[],這種情況下只有在元件初始化或銷燬的時候才會觸發,用來代替 componentDidMount 和 componentWillUnmount,慎用;
    1. 還有另外一個情況,就是不傳遞第二個引數,也就是useEffect只接收了第一個函式引數,代表不監聽任何引數變化。每次渲染DOM之後,都會執行useEffect中的函式。
import React, { useState, useEffect } from "react";
let timer = null;
function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = "componentDidMount" + count;
  },[count]);

  useEffect(() => {
    timer = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
    // 一定注意下這個順序:
    // 告訴react在下次重新渲染元件之後,同時是下次執行上面setInterval之前呼叫
    return () => {
      document.title = "componentWillUnmount";
      clearInterval(timer);
    };
  }, []);

  return (
    <div>
      Count: {count}
      <button onClick={() => clearInterval(timer)}>clear</button>
    </div>
  );
}

3、useContext 減少元件層級

用來代替:Provider, Consumer
處理多層級傳遞資料的方式,在以前元件樹種,跨層級祖先元件想要給孫子元件傳遞資料的時候,除了一層層 props 往下透傳之外,我們還可以使用 React Context API 來幫我們做這件事

類元件案例

const { Provider, Consumer } = React.createContext(null);
function Bar() {
  return <Consumer>{color => <div>{color}</div>}</Consumer>;
}

function Foo() {
  return <Bar />;
}

function App() {
  return (
    <Provider value={"grey"}>
      <Foo />
    </Provider>
  );
}

useContext使用的方法

  1. 要先建立createContex
    使用createContext建立並初始化
    import  { createContext } from 'react'
    const C = createContext(null);
    
  2. Provider 指定使用的範圍
    在圈定的範圍內,傳入讀操作和寫操作物件,然後可以使用上下文
        <C.Provider value={{n,setN}}>
        這是爺爺
        <Baba></Baba>
        </C.Provider>
    
  3. 最後使用useContext
    使用useContext接受上下文,因為傳入的是物件,則接受的也應該是物件
    const {n,setN} = useContext(C);
    
import React, { createContext, useContext, useReducer, useState } from 'react'
import ReactDOM from 'react-dom'
// 創造一個上下文
const C = createContext(null);
function App(){
  const [n,setN] = useState(0)
  return(
    // 指定上下文使用範圍,使用provider,並傳入讀資料和寫入據
    <C.Provider value={{n,setN}}>
      這是爺爺
      <Baba></Baba>
    </C.Provider>
  )
}


function Baba(){
  return(
    <div>
      這是爸爸
      <Child></Child>
    </div>
  )
}

function Child(){
  // 使用上下文,因為傳入的是物件,則接受也應該是物件
  const {n,setN} = useContext(C)
  const add=()=>{
    setN(n=>n+1)
  };
  return(
    <div>
      這是兒子:n:{n}
      <button onClick={add}>+1</button>
    </div>
  )
}

ReactDOM.render(<App />,document.getElementById('root'));

4、useReducer 資料互動

用來代替:Redux/React-Redux
唯一缺少的就是無法使用 redux 提供的中介軟體

import React, { useReducer } from "react";

const initialState = {
  count: 0
};

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

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

5、useCallback 記憶函式

減少重複渲染
老規矩,第二個引數傳入一個陣列,陣列中的每一項一旦值或者引用發生改變,useCallback 就會重新返回一個新的記憶函式提供給後面進行渲染。

這樣只要子元件繼承了 PureComponent 或者使用 React.memo 就可以有效避免不必要的 VDOM 渲染。

import React, { useState, useCallback, memo } from 'react'

const Child = memo(function(props) {
  console.log('child run...')
  return (
    <>
      <h1>hello</h1>
      <button onClick={props.onAdd}>add</button>
    </>
  )
})

export default function UseCallback() {
  console.log('parent run...')
  let [ count, setCount ] = useState(0)

  const handleAdd = useCallback(
    () => {
      console.log('added.')
    },
    [],
  )
  return (
    <div>
      <div>{count}</div>
      <Child onAdd={handleAdd}></Child>
      <button onClick={() => setCount(100)}>change count</button>
    </div>
  )
}

6、useMemo 記憶元件

useCallback 的功能完全可以由 useMemo 所取代,如果你想通過使用 useMemo 返回一個記憶函式也是完全可以的。
區別:useCallback 不會執行第一個引數函式,而是將它返回給你,而 useMemo 會執行第一個函式並且將函式執行結果返回給你
function App() { const memoizedHandleClick = useMemo(() => () => { console.log('Click happened') }, []); // 空陣列代表無論什麼情況下該函式都不會發生改變 return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>; }

  • useCallback: 常用記憶事件函式,生成記憶後的事件函式並傳遞給子元件使用。
  • useMemo: 更適合經過函式計算得到一個確定的值,比如記憶元件。
    function Parent({ a, b }) {
        const child1 = useMemo(() => () => <Child1 a={a} />, [a]);
        const child2 = useMemo(() => () => <Child2 b={b} />, [b]);
        return (
            <>
            {child1}
            {child2}
            </>
        )
    }
    

7、useRef 儲存引用值

用來代替:createRef

Ref
React提供了一個屬性ref,用於表示對組價例項的引用,其實就是ReactDOM.render()返回的元件例項:

  • ReactDOM.render()渲染元件時返回的是元件例項;
  • 渲染dom元素時,返回是具體的dom節點。

ref可以掛載到元件上也可以是dom元素上;

  • 掛到元件(class宣告的元件)上的ref表示對元件例項的引用。不能在函式式元件上使用 ref 屬性,因為它們沒有例項:
  • 掛載到dom元素上時表示具體的dom元素節點。

useRef()
useRef這個hooks函式,除了傳統的用法之外,它還可以“跨渲染週期”儲存資料。

  • 可以通過 ref.current 值訪問元件或真實的 DOM 節點,從而可以對 DOM 進行一些操作,比如監聽事件等等
    export default function App() {
          const count = useRef(0)
          <!-- count.current儲存狀態值 -->
          const handleClick = (num) => {
              count.current += num
              setTimeout(() => {
              console.log("count: " + count.current);
              }, 3000)
          }
    
          return (
              <div>
              <p>You clicked {count.current} times</p>
              <button onClick={() => handleClick(1)}>增加 count</button>
              <button onClick={() => handleClick(-1)}>減少 count</button>
              </div>
          );
      }
    
    

8、useImperativeHandle 透傳 Ref

通過 useImperativeHandle 用於讓父元件獲取子元件內的索引
useImperativeHandle 應當與 forwardRef 一起使用
useImperativeHandle(ref, createHandle, [deps])

  • ref:定義 current 物件的 ref createHandle:一個函式,返回值是一個物件,即這個 ref 的 current
  • [deps]:即依賴列表,當監聽的依賴發生變化,useImperativeHandle 才會重新將子元件的例項屬性輸出到父元件
  • 注意:ref 的 current 屬性上,如果為空陣列,則不會重新輸出。
import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";

function ChildInputComponent(props, ref) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => inputRef.current);
  return <input type="text" name="child input" ref={inputRef} />;
}

const ChildInput = forwardRef(ChildInputComponent);

function App() {
  const inputRef = useRef(null);
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <div>
      <ChildInput ref={inputRef} />
    </div>
  );
}

export default App

9、useLayoutEffect 同步執行副作用

大部分情況下,使用 useEffect 就可以幫我們處理元件的副作用,但是如果想要同步呼叫一些副作用,比如對 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用會在 DOM 更新之後同步執行。
(1) useEffect和useLayoutEffect有什麼區別?

簡單來說就是呼叫時機不同

  • useLayoutEffect:和原來componentDidMount&componentDidUpdate一致,在react完成DOM更新後馬上同步呼叫的程式碼,會阻塞頁面渲染。
  • useEffect:是會在整個頁面渲染完才會呼叫的程式碼。
  • 官方建議優先使用useEffect

在實際使用時如果想避免頁面抖動(在useEffect裡修改DOM很有可能出現)的話,可以把需要操作DOM的程式碼放在useLayoutEffect裡。關於使用useEffect導致頁面抖動。
不過useLayoutEffect在服務端渲染時會出現一個warning,要消除的話得用useEffect代替或者推遲渲染時機。

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

export default function (props) {
  let [data, setData] = useState({count: 0})

  function loadData() {
    return fetch('http://localhost:8080/api/movies/list')
      .then(response => response.json())
      .then(result => {
        return result
      })
  }

  useEffect(() => {
    console.log('effect')
  }, [data])

  useEffect(() => {
    console.log('mounted.')

    ;(async ()=>{
      let result = await loadData()
      console.log(result)
    })()

    // return () => {
    //   console.log('unmout')
    // }
  }, [])

  return (
    <>
      <div>{data.count}</div>
      <button onClick={() => setData(data => ({count: data.count + 1}))}>click</button>
    </>
  )
}

相關文章