1. 程式人生 > 實用技巧 >React Hooks --- useState 和 useEffect

React Hooks --- useState 和 useEffect

React Hooks --- useState 和 useEffect

  React Hooks 都是函式,使用React Hooks,就是呼叫函式。React Hooks在函式元件中使用,當React渲染函式元件時,元件裡的每一行程式碼就會依次執行,一個一個的Hooks也就依次呼叫執行。

  useState(): 接受一個引數,返回了一個數組。

  引數:可以是任意型別。基本型別,物件,函式都沒有問題。作用呢?就是給元件設定一個初始的狀態。當元件初次渲染時,它要顯示什麼,這個引數就是什麼。

  返回值:一個數組。陣列的第一項是元件的狀態,第二項是更新狀態的函式,那麼在元件中就可以宣告一個變數來儲存狀態,一個變數來儲存更改狀態的函式,至此函式元件中就有了狀態,確切的說是,元件中擁有一個狀態變數,你可以隨時更改它的值,元件的狀態就是某一時刻的變數的值。更新狀態的函式就是用來改變這個變數的值的。

  做一個input 輸入框元件,初始狀態是空字串,那麼傳給useState的引數就是""。呼叫useState() 函式會返回一個數組,那就宣告一個變數,進行接收,再從陣列中就獲取狀態和更新函式。

function App() {
    const arr = useState("");
    const state = arr[0];
    const setState = arr[1];
}

  可以看到,useState() hook的使用和普通函式沒什麼區別,都是傳遞引數,接收返回值。不過,這麼寫有點麻煩了,使用陣列解構賦值會簡潔一些,最好也為狀態變數和更新函式起一個有意義的名字

const App= () => {
  const [message, setMessage]= useState('');
}

  有了狀態變數之後,就可以在函式元件中使用了,變數的使用沒有任何區別,就是在某個地方引用它,獲取它的值。比如,在jsx中引用它,元件狀態就可以渲染到頁面上。

使用create-react-app建立專案,修改App.js

const App = () => {
  const [message, setMessage]= useState('');

  return (
    <input value={message}></input>
  )
}

  npm start,頁面上有了一個空輸入框。元件渲染時,執行第一行程式碼,呼叫useState(), 返回了初始狀態(空字串),賦值給了message變數。接著向下執行,返回一個jsx,它裡面使用了message ,賦值給value,那就讀取這時候的message變數的值賦值給value, message變數的值這時為空字串,value的值也就為空字串。 渲染完成後,頁面中顯示了一個input輸入框,值為空。增加一下互動性,更好地理解useState和元件的渲染過程,給input新增onChange事件

const App = () => {
  const [message, setMessage]= useState('');

  function handleChange(e) {
    setMessage(e.target.value)
  }
  return (
    <input value={message} onChange={handleChange}></input>
  )
}

  input中輸入1,觸發了onChange事件,呼叫setMessage, React在內部重新計算了狀態值,知道狀態改變了,觸發了React的更新機制。因為setMessage()函式也是React暴露給我們的,我們呼叫函式,把最新值傳給了React, React內部就會執行這個函式,計算出新的狀態值,並儲存起來。可以這麼簡單理解一個useState

let _val;
function useState(initState) {
    _val = initState;

    function setState(value) {
        _val = value
    }

    return [_val, setState];
}

  當然React不會立刻更新元件,而是把它放到更新佇列中,和類元件中的setState一樣,React的渲染是非同步的。當真正重新渲染時,React 又會呼叫App函式元件,還是從上到下,一行一行執行程式碼。先呼叫useState(), 不過這時useState不是返回初始值,函式的引數被忽略了,而是返回觸發更新的setMessage中的值e.target.value。因為呼叫setMessage時,我們向React傳遞了一個引數,React在內部完成了狀態更新並儲存。再次呼叫useState()時,它返回的就是更新後的值。把useState返回的值,也就是你在輸入框中輸入的值1,賦值給了message.接著向下執行,一個函式的建立,然後是jsx,jsx中的message 取當前值為1,然後賦值給value,渲染完成,頁面上input中顯示1。當你再輸入2的時候,更新函式再次呼叫,React內部再次執行更新函式,並儲存最新狀態。App 元件再次被呼叫,還是先執行useSate() 返回最新的狀態12,賦值給message, 然後建立一個handleClick 函式,最後jsx 中message 取12, 元件渲染完成後,頁面中的輸入框中顯示12. 整個過程如下

// 初始渲染。
const message = '';  // useState() 的呼叫
function handleChange(e) {
  setMessage(e.target.value)
}
return (
  <input value='' onChange={handleChange}></input>
)

// 輸入1 更新渲染
const message = 1;  // useState() 的呼叫
function handleChange(e) {
  setMessage(e.target.value)
}
return (
  <input value=1 onChange={handleChange}></input>
)

// 再次輸入2,更新渲染
const message = 12;  // useState() 的呼叫
function handleChange(e) {
  setMessage(e.target.value)
}
return (
  <input value=12 onChange={handleChange}></input>
)

  元件每一次渲染,都會形成它自己獨有的一個版本,在每次渲染中,都擁有著屬於它本次渲染的狀態和事件處理函式,每一次的渲染都是相互隔離,互不影響的。狀態變數,也只是一個普通的變數,甚至在某一次渲染中,可以把它看成一個擁有某個值的常量。它擁用的這個值,正好是react 的useState 提供給我們的。React 負責狀態的管理,而我們只是宣告變數,使用狀態。狀態的更新,只不過是元件的重新渲染,React 重新呼叫了元件函式,重新獲取useState返回的值。useState() 返回的永遠都是最新的狀態值。

  一定要注意useState的引數,它只有在第一次渲染的時候起作用,給狀態變數賦初始值,使元件擁有初始狀態。在以後的渲染中,不管是呼叫更新函式導致的元件渲染,還是父元件渲染導致的它的渲染,引數都不會再使用了,直接被忽略了,元件中的state狀態變數,獲取的都是最新值。如果你想像下面的程式碼一樣,使用父元件每次傳遞過來的props 來更新state,

const Message= (props) => {
   const messageState = useState(props.message);
    /* ... */
}

  就會有問題,因為props.message, 只會在第一次渲染中使用,以後元件的更新,它就會被忽略了。useState的引數只在初次渲染的時候使用一次,有可能也是useState 可以接受函式的原因,因為有時候,元件初始狀態,是需要計算的,比如 我們從localStorage中去取資料作為初始狀態。如果在元件中直接寫

const Message= (props) => {

let name = localStorage.getItem('name');
const messageState = useState(name);
/* ... */
}

  那麼元件每一次的渲染都會呼叫getItem, 沒有必要,因為我們只想獲取初始狀態,呼叫一次就夠了。useState如果接受函式就可以解決這個問題,因為它的引數,就是隻在第一次渲染時才起作用,對於函式來說,就是在第一次渲染的時候,才會呼叫函式,以後都不會再呼叫了。

const Message= (props) => {
const messageState = useState(() => {return localstorage.getItem('name')});
/* ... */
}

  更新函式的引數還可以是函式,函式引數是前一個狀態的值。如果你想使用以前的狀態生成一個新的狀態,最好使用函式作為更新函式的引數。

function handleChange(e){
   const val = e.target.value;
   setMessage(prev => prev + val);
}

  當元件的狀態是引用型別,比如陣列和物件的時候,情況要稍微複雜一點,首先我們不能只更改這個狀態變數的屬性值,我們要生成一個新的狀態值。

const App = () => {
    const [messageObj, setMessage] = useState({ message: '' }); // 狀態是一個物件

    function handleChange(e) {
        messageObj.message = e.target.value; // 只是改變狀態的屬性
        setMessage(messageObj)
    }
    return (
        <input type="text" value={messageObj.message} onChange={handleChange}/>
    );
};

  無法在input中輸入內容。React更新狀態時,會使用Object.js()對新舊狀態進行比較,如果它倆相等,就不會重新渲染元件。物件的比較是引用的比較,相同的引用, React 不會重新渲染。所以handleChange 要改成如下

 function handleChange(e) {
        const newMessageObj = { message: e.target.value }; // 重新生成一個物件
        setMessage(newMessageObj);
    }

  這又引出了另外一個問題,react 狀態更新使用的是整體替換原則,使用新的狀態去替換掉老的狀態,而不是setState的合併原則。如果使用setState,我們只需要setState那些要改變的狀態就可以了,React會把這次所做的改變和原來沒有做改變的狀態進行合併,形成新的整個元件的狀態。但這裡的setMessage()不行,

const App = () => {
    const [messageObj, setMessage] = useState({ message: '', id: 1 });

    return (
        <div>
            <input value={messageObj.message}
                onChange={e => {
                    const newMessageObj = { message: e.target.value };
                    setMessage(newMessageObj); 
                }}
            />
            <p>{messageObj.id} : {messageObj.message}</p>
        </div>
    );
};

  在輸入框中輸入內容的時候,發現id 屬性不見了,新的狀態去替換掉了整個舊的狀態。onChange 要修改如下

onChange = { e => {
    const val = e.target.value;
    setMessage(prevState => {
        return { ...prevState, message: val }
    });
}}

  也正因為如此,React 建議我們把複雜的狀態進行拆分,拆成一個一個單一的變數,更新的時候,只更新其中的某個或某些變數。就是使用多個useState(), 生成多個狀態變數和更新函式。

const App = () => {
    const [message, setMessage] = useState('');
    const [id, setId] = useState(1);

    return (
        <div>
            <input value={message}
                onChange={e => {
                    setMessage(e.target.value); 
                }}
            />
            <p>{id} : {message}</p>
        </div>
    );
};

  當然,複雜狀態變數(比如,Object 物件)可以拆分,主要是物件的各個屬性之間的關聯不大。如果物件的各個屬性關聯性特別強,就必須是一個複雜物件的時候,建議使用useReducer.

  useEffect()

  React 的世界裡,不是隻有狀態和改變狀態,它還要和外界進行互動,最常見的就是和伺服器進行互動,傳送ajax請求。這部分程式碼放到什麼地方呢?使用useEffect(). 元件渲染完成後,你想做什麼?就把什麼放到useEffect()中,因此,useEffect 的第一個引數就是一個回撥函式,包含你要做的事情。元件渲染完成了,要請求資料,那就把請求資料內容放到useEffect 的回撥函式中。等到元件真正渲染完成後, 回撥函式自動呼叫,資料請求,就傳送出去了。使用一下JSONPlaceholder,給輸入框賦值

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

export default function App() {
    const [message, setMessage]= useState('');

    function handleChange(e) {
        setMessage(e.target.value)
    }
    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
            .then(response => response.json())
            .then(json => {
                console.log(json);
                setMessage(json.title);
            })
    })

    return <input value={message} onChange={handleChange}></input>
}

  開啟控制檯,可以發現介面呼叫了兩次,當輸入的時候,更是奇怪,直接輸入不了,它在不停地呼叫介面。這時,你可能想到原因了,狀態更新會導致元件重新渲染,渲染就會用完成時,完成的那一剎那,useEffect又會重新呼叫。只要元件渲染完成,不管是初次渲染,還是狀態更新導致的重新渲染,useEffect 都會被呼叫。那不就有問題了嗎?請求資料-> 更新狀態->重新請求資料->更新狀態,死迴圈了。這就用到了useEffect的第二個引數,一個數組,用來告訴React ,渲染完成後,要不要呼叫useEffect中的函式。怎樣使用陣列進行告知呢?就把useEffect回撥函式中的要用到的外部變數或引數,依次寫到陣列中。那麼React就知道回撥函式的執行是依賴這些變數的,那麼它就會時時地監聽這些變數的變化,只要有更新,它就會重新呼叫useEfffect.這個陣列因此也稱為依賴數陣列,回撥函式要再次執行的依賴。現在看一下我們的回撥函式fetch, 裡面的內容都是寫死的,沒有任何外部變數依賴,那就寫一個空陣列。React看到空陣列,也就明白了,useEffect中的回撥函式不依賴任何變數,那它就執行一遍就好了。初次渲染進行執行,以後更新就不用管了。

    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/1')
            .then(response => response.json())
            .then(json => {
                console.log(json);
                setMessage(json.title);
            })
    }, []) // 空陣列,回撥函式沒有依賴作何外部的變數

  有的時候,不能只獲取1(id)的todos,使用者傳遞出來的id是幾,就要顯示id是幾的 todos. 那麼fetch的url就不是固定的了,而是變化的了。useEffect的回撥函式也就有了依賴了,那就是一個id,這個id是需要外界傳遞過來的,useEffect的回撥函式中用到了一個外部的變數id,那就需要把id寫到依賴陣列中。再寫一個input表示使用者傳遞過來的id

export default function App() {
    const [todoTitle, setTodoTitle]= useState('');
    const [id, setId] = useState(1);

    function handleChange(e) {
        setTodoTitle(e.target.value)
    }
    function handleId(e) {
        setId(e.target.value);
    }
    useEffect(() => {
        fetch('https://jsonplaceholder.typicode.com/todos/' + id)
            .then(response => response.json())
            .then(json => {
                setTodoTitle(json.title);
            })
    }, [id]) // 回撥函式依賴了一個外部變數id

    return( 
        <>
            <p>id:  <input value={id} onChange={handleId}></input></p>  
            <p>item title: <input value={todoTitle} onChange={handleChange}></input> </p>
        </>
    )
}

  可以把陣列中的id去掉,測試一下效果,只有初次載入的時候,傳送了請求,以後不管你輸入什麼,再也不會發送請求了。