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 去掉,測試一下效果,只有初次加載的時候,發送了請求,以后不管你輸入什么,再也不會發送請求了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM