實戰專案後對 React Hooks 總結

叫我小明呀發表於2019-07-08

什麼是 useState ?

首先 useState 是一個Hook,它允許您將React狀態新增到功能元件

useState 是一個方法,它本身是無法儲存狀態的

其次,他執行在 FunctionalComponent 裡面,本身也是無法儲存狀態的

useState 只接收一個引數 inital value,並看不出有什麼特殊的地方。

為什麼要 useState?

因為類元件有很多的痛點

  1. 很難複用邏輯(只能用HOC,或者render props),會導致元件樹層級很深
  2. 會產生巨大的元件(指很多程式碼必須寫在類裡面)
  3. 類元件很難理解,比如方法需要bindthis指向不明確 比如 經常看到這樣的寫法。
// 可能是這樣
class MyComponent extends React.Component {
  constructor() {
    // initiallize
    this.handler1 = this.handler1.bind(this)
    this.handler2 = this.handler2.bind(this)
    this.handler3 = this.handler3.bind(this)
    this.handler4 = this.handler4.bind(this)
    this.handler5 = this.handler5.bind(this)
    // ...more
  }
}

// 可能是這樣的
export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
複製程式碼

怎麼用 useState?

開始之前先看一個簡單的例子,在沒有 Hooks 之前我們是這樣來寫的。

import React, {Component} from 'react';
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      Switch: "開啟"
    };
  }
  setSwitch = () => {
    this.state.Switch === "開啟"
      ? this.setState({ Switch: "關閉" })
      : this.setState({ Switch: "開啟" });
  };
  render() {
    return (
      <div>
        <p>現在是: {this.state.Switch}狀態</p>
        <button onClick={this.setSwitch}>Change Me!</button>
      </div>
    );
  }
}
export default CommonCollectionPage;
複製程式碼

以前函式式元件需要給他自己的狀態的時候我們總是不得不把函式式元件變成 Class 類元件,現在有了 React Hooks 我們在也不需要因為一個小狀態而將函式式元件變成類元件,上面的這個例子,就可以變成下面的這個方法來表現。

useState 使用案例

function App() {
  const [Switch, setSwitch] = useState("開啟");
  const newName = () =>
    Switch === "開啟" ? setSwitch("關閉") : setSwitch("開啟");
  return (
    <div>
      <p>現在是: {Switch} 狀態</p>
      <button onClick={newName}>Change Me!</button>
    </div>
  );
}
複製程式碼

所以 useState 就是為了給函式式元件新增一個可以維護自身狀態的功能。

操作地址傳送

useState 注意事項

動態傳遞引數給 useState 的時候只有第一次才會生效

useEffect 是什麼?

通過 useEffect 方法來代替了 class 類元件的 componentDidMount, componentDidUpdate, componentWillUnmount

三個元件,那如何一個鉤子怎麼使用才有 3 個鉤子的不同效果呢:

首選我們先執行起來:

useEffect 第一個引數

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [switch, setSwitch] = useState("開啟");
  const handleSwitch = _ =>
    switch === "開啟" ? setSwitch("關閉") : setSwitch("開啟");
  const [num, setNum] = useState(0);
  const add = () => {
    setNum(num + 1);
  };
  const minus = () => {
    setNum(num - 1);
  };
  useEffect(() => {
    console.log("改變了狀態");
  }, [num]);
  return (
    <div>
      <p>現在是: {switch}狀態</p>
      <button onClick={handleSwitch}>Change Me!</button>
      <p>數字: {num}</p>
      <button onClick={add}> +1 </button>
      <button onClick={minus}> -1 </button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
複製程式碼

上面這個例子他會再初次渲染的時候列印 改變了狀態 並且每次狀態改變的時候都列印 改變了狀態

那麼這樣的用法就是 componentDidMount, componentDidUpdate,的使用

useEffect 第二個引數

useEffect 方法中加入第二個引數 一個空物件 [ ] 那麼之後只會在首次元件裝載好的時候列印一次

改變狀態了

現在我們需要當 num 改變狀態的時候 去列印 怎麼辦呢?

剛剛我們使用了一個空物件 那麼只需要再這個物件中加入我們需要監聽的狀態那麼他相當於使用了 , componentDidUpdate, 鉤子函式例如

useEffect(() => {
    console.log("改變了狀態");
  }, [num]);
複製程式碼

那麼現在,當 activeUser 狀態改變的時候我們會發現又列印出了 改變狀態 這句話。而當 switch 狀態改變的時候並不會列印這句話。

useEffect 的閉包使用

import React, { useState, useEffect } from 'react';
function App() {
  useEffect(() => {
    console.log('裝載了')
    return () => {
      console.log('解除安裝拉');
    };
  });
  return (
    <div>
      xxxx
    </div>
  );
}
export default App;
複製程式碼

在 useEffect 中我們可以做兩件事情,元件掛載完成時候,還有元件解除安裝時,只要在 useEffect 中使用閉包,在閉包中做我們想要在元件解除安裝時需要做的事就可以。

this.state 和 useState

首先我們回顧下以前我們常常用的類元件,下面是一段實現計數器的程式碼:

import React, { Component, useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

class App extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    setTimeout(() => {
      console.log(`count:${this.state.count}`);
    }, 3000);
  }
  componentDidUpdate() {
    setTimeout(() => {
      console.log(`count:${this.state.count}`);
    }, 3000);
  }
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button
          onClick={() =>
            this.setState({
              count: this.state.count + 1
            })
          }
        >
          點選 3 次
        </button>
      </div>
    );
  }
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
複製程式碼

頁面重新整理立即,點選 3 次按鈕,上述這個例子的列印結果會是什麼????

我們很清楚的瞭解 this.state 和 this.setState 所有我們會知道列印的是:

一段時間後依此列印 3,3,3,3

不過 hooks 中 useEffect 的執行機制並不是這樣運作的。

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      console.log(count);
    }, 3000);
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        點選 3 次
      </button>
    </div>
  );
}
複製程式碼

一段時間後依此列印 0,1,2,3。

其實沒有以前 this.state 使用印象,看到這段程式碼的列印結果,會認為這不是理所當然的嗎?

那我們想讓上面的類元件,也實現上述 0,1,2,3,4 效果 我們增加這 2 行程式碼

//...

componentDidMount() {
    // 新增此行程式碼
    const newCount = this.state.count; 
    setTimeout(() => {
      console.log(newCount);
    }, 3000);
  }
  componentDidUpdate() {
    // 新增此行程式碼
    const newCount = this.state.count; 
    setTimeout(() => {
      console.log(newCount);
    }, 3000);
  }

  // ....
複製程式碼

所有我們會聯想到 在函式式元件中 取值下面的效果是一樣的

function Counter(props) {
  useEffect(() => {
    setTimeout(() => {
      console.log(props.counter);
    }, 3000);
  });
  // ...
}
複製程式碼
function Counter(props) {
  const counter = props.counter;
  useEffect(() => {
    setTimeout(() => {
      console.log(counter);
    }, 3000);
  });
  // ...
}
複製程式碼

這一點說明了在渲染函式式元件的時候他的更新不會改變渲染範圍內的 props ,state 的值。(表達可能有誤)

當然,有時候我們會想在effect的回撥函式裡讀取最新的值而不是之前的值。就像之前的類元件那樣列印 3.3.3.3。這裡最簡單的實現方法是使用useRef。

useRef 的使用方式

首先實現上訴列印 3,3,3,3 的問題。如下程式碼所示

function Counter() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);
  useEffect(() => {
    latestCount.current = count;
    setTimeout(() => {
      console.log(latestCount.current);
    }, 3000);
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        點選 3 次
      </button>
    </div>
  );
}
複製程式碼

我們通過 useRef(initVal) 來返回一個可變的 ref 物件,其 current 屬性被初始化為傳遞的引數 (initVal)。

useRef 也可以獲取 DOM 節點

然後函式式元件沒有生命週期,那我們怎麼才能獲取 ChartDom 真實的 dom 元素呢?也可以通過 useRef 實現

import React, { useState, useRef, Fragment, useEffect } from 'react';
import { Button } from 'antd';

function Demo({ count: propsCount = 1 }) {
  const [count, setCount] = useState(propsCount);
  const refContainer = useRef(null); // 如同之前的 React.createRef();
  
  useEffect(() => {
    console.log(refContainer.current, '>>>>>>>>>>>');
  });
  
  return (
      <Fragment>
        <Button onClick={() => { setCount(count + 1); }}>Click Me</Button>
        <p ref={refContainer}>You click {count} times</p>
      </Fragment>
  );
}

export default Demo;
複製程式碼

useReducer 使用

import React, { useReducer, useEffect } from "react";
import ReactDOM from "react-dom";

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}
複製程式碼

useImperativeHandle 使用

在類元件中我們都是通過使用 ref 的方式 獲取類元件的例項,這樣就可以讓父元件呼叫子元件的方法。

那麼函式式沒有例項,怎麼使用 ref 呢?

實戰專案後對 React Hooks 總結

// 子元件
import React, { useRef, useImperativeHandle,forwardRef } from "react";
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
export default forwardRef(FancyInput);


複製程式碼
// 父元件
import React, { useRef } from "react";
function App(){
  const fancyInputRef = useRef(null)
  // 這樣獲取子元件方法
  fancyInputRef.current.focus()
  return (
    <div> 
      <FancyInput ref={fancyInputRef} /> 
    </div>
  )

}
複製程式碼

最後

全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

個人其他文章推薦:

  1. Axios 原始碼解讀
  2. Fetch 例項講解

參考:

相關文章