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