react之react Hooks

函數組件,沒有 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('//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>
    </>
  )
}