可以在項目中使用的有用的 React Hooks

已發表: 2022-03-10
快速總結↬基於類的 React 組件對人和機器來說都是混亂、混亂、困難的。 但在 React 16.8 之前,任何需要狀態、生命週期方法和許多其他重要功能的項目都必須使用基於類的組件。 隨著 React 16.8 中鉤子的引入,所有這些都發生了變化。 Hooks 是遊戲規則的改變者。 他們簡化了 React,使其更整潔、更易於編寫和調試,並且還減少了學習曲線。

Hooks 是簡單的函數,允許你連接使用React 特性。 它們在 React Conf 2018 上被引入,以解決類組件的三個主要問題:包裝器地獄、巨大的組件和令人困惑的類。 Hooks 為 React 功能組件提供了強大的功能,使得使用它開發整個應用程序成為可能。

前面提到的類組件的問題是相互聯繫的,解決一個而沒有另一個可能會引入更多的問題。 值得慶幸的是,鉤子簡單有效地解決了所有問題,同時為 React 中更有趣的功能創造了空間。 Hooks 不會取代現有的 React 概念和類,它們只是提供一個 API 來直接訪問它們。

React 團隊在 React 16.8 中引入了幾個鉤子。 但是,您也可以在應用程序中使用來自第三方提供商的掛鉤,甚至創建自定義掛鉤。 在本教程中,我們將看看 React 中一些有用的鉤子以及如何使用它們。 我們將介紹每個鉤子的幾個代碼示例,並探討如何創建自定義鉤子。

注意:本教程需要對 Javascript (ES6+) 和 React 有基本的了解。

跳躍後更多! 繼續往下看↓

鉤子背後的動機

如前所述,創建鉤子是為了解決三個問題:包裝器地獄、巨大的組件和令人困惑的類。 讓我們更詳細地看一下其中的每一個。

包裝地獄

使用類組件構建的複雜應用程序很容易陷入包裝地獄。 如果您在 React 開發工具中檢查應用程序,您會注意到嵌套很深的組件。 這使得使用組件或調試它們變得非常困難。 雖然這些問題可以通過高階組件渲染道具來解決,但它們需要您稍微修改一下代碼。 這可能會導致複雜應用程序的混亂。

Hooks 易於共享,您不必在重用邏輯之前修改您的組件。

一個很好的例子是使用 Redux connect高階組件(HOC)來訂閱 Redux 存儲。 像所有 HOC 一樣,要使用 connect HOC,您必須將組件與定義的高階函數一起導出。 在connect的情況下,我們會有這種形式的東西。

 export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

其中mapStateToPropsmapDispatchToProps是要定義的函數。

而在 Hooks 時代,使用 Redux 的useSelectoruseDispatch hooks 可以輕鬆簡潔地實現相同的結果。

龐大的組件

類組件通常包含副作用和有狀態的邏輯。 隨著應用程序複雜性的增加,組件變得混亂和混亂是很常見的。 這是因為預計副作用將通過生命週期方法而不是功能來組織。 雖然可以拆分組件並使它們更簡單,但這通常會引入更高級別的抽象。

掛鉤按功能組織副作用,並且可以根據功能將組件拆分為多個部分。

令人困惑的課程

類通常是比函數更難的概念。 React 基於類的組件很冗長,對初學者來說有點困難。 如果你是 Javascript 新手,你會發現函數比類更容易上手,因為它們的語法輕量級。 語法可能令人困惑; 有時,可能會忘記綁定可能破壞代碼的事件處理程序。

React 通過功能組件和鉤子解決了這個問題,讓開發人員可以專注於項目而不是代碼語法。

例如,以下兩個 React 組件將產生完全相同的結果。

 import React, { Component } from "react"; export default class App extends Component { constructor(props) { super(props); this.state = { num: 0 }; this.incrementNumber = this.incrementNumber.bind(this); } incrementNumber() { this.setState({ num: this.state.num + 1 }); } render() { return ( <div> <h1>{this.state.num}</h1> <button onClick={this.incrementNumber}>Increment</button> </div> ); } }
 import React, { useState } from "react"; export default function App() { const [num, setNum] = useState(0); function incrementNumber() { setNum(num + 1); } return ( <div> <h1>{num}</h1> <button onClick={incrementNumber}>Increment</button> </div> ); }

第一個示例是基於類的組件,而第二個示例是功能組件。 儘管這是一個簡單的示例,但請注意第一個示例與第二個示例相比有多麼虛假。

Hooks 公約和規則

在深入研究各種鉤子之前,看看適用於它們的約定和規則可能會有所幫助。 以下是適用於鉤子的一些規則。

  1. 掛鉤的命名約定應以前綴use開頭。 因此,我們可以有useStateuseEffect等。如果您使用的是 Atom 和 VSCode 等現代代碼編輯器,ESLint 插件可能是 React hooks 的一個非常有用的功能。 該插件提供了有關最佳實踐的有用警告和提示。
  2. 鉤子必須在組件的頂層調用,在 return 語句之前。 它們不能在條件語句、循環或嵌套函數中調用。
  3. 必須從 React 函數(在 React 組件或另一個鉤子內部)調用鉤子。 不應從 Vanilla JS 函數調用它。

useState鉤子

useState鉤子是最基本和最有用的 React 鉤子。 像其他內置的鉤子一樣,這個鉤子必須從react中導入才能在我們的應用程序中使用。

 import {useState} from 'react'

要初始化狀態,我們必須聲明狀態及其更新函數並傳遞一個初始值。

 const [state, updaterFn] = useState('')

我們可以隨意調用我們的狀態和更新函數,但按照慣例,數組的第一個元素將是我們的狀態,而第二個元素將是更新函數。 一種常見的做法是在我們的更新函數前面加上前綴,然後是我們的狀態名稱,採用駝峰式形式。

例如,讓我們設置一個狀態來保存計數值。

 const [count, setCount] = useState(0)

請注意,我們的count狀態的初始值設置為0而不是空字符串。 換句話說,我們可以將我們的狀態初始化為任何類型的 JavaScript 變量,即數字、字符串、布爾值、數組、對象,甚至 BigInt。 使用useState鉤子設置狀態和基於類的組件狀態之間有明顯的區別。 值得注意的是useState鉤子返回一個數組,也稱為狀態變量,在上面的示例中,我們將數組解構為stateupdater函數。

重新渲染組件

使用useState鉤子設置狀態會導致相應的組件重新呈現。 然而,這只有在 React 檢測到先前或舊狀態與新狀態之間的差異時才會發生。 React 使用 Javascript Object.is算法進行狀態比較。

使用useState設置狀態

我們的count狀態可以設置為新的狀態值,只需將新值傳遞給setCount更新器函數,如下所示setCount(newValue)

當我們不想引用先前的狀態值時,此方法有效。 如果我們希望這樣做,我們需要將一個函數傳遞給setCount函數。

假設我們想在每次單擊按鈕時向count變量添加 5,我們可以執行以下操作。

 import {useState} from 'react' const CountExample = () => { // initialize our count state const [count, setCount] = useState(0) // add 5 to to the count previous state const handleClick = () =>{ setCount(prevCount => prevCount + 5) } return( <div> <h1>{count} </h1> <button onClick={handleClick}>Add Five</button> </div> ) } export default CountExample

在上面的代碼中,我們首先從react中導入useState鉤子,然後使用默認值 0 初始化count狀態。我們創建了一個onClick處理程序,以在單擊按鈕時將count的值增加 5。 然後我們將結果顯示在h1標籤中。

設置數組和對象狀態

數組和對象的狀態可以以與其他數據類型大致相同的方式設置。 但是,如果我們希望保留已經存在的值,我們需要在設置狀態時使用 ES6 擴展運算符。

Javascript 中的擴展運算符用於從現有對象創建新對象。 這在這裡很有用,因為React將狀態與Object.is操作進行比較,然後相應地重新渲染。

讓我們考慮下面的代碼來設置按鈕單擊的狀態。

 import {useState} from 'react' const StateExample = () => { //initialize our array and object states const [arr, setArr] = useState([2, 4]) const [obj, setObj] = useState({num: 1, name: 'Desmond'}) // set arr to the new array values const handleArrClick = () =>{ const newArr = [1, 5, 7] setArr([...arr, ...newArr]) } // set obj to the new object values const handleObjClick = () =>{ const newObj = {name: 'Ifeanyi', age: 25} setObj({...obj, ...newObj}) } return( <div> <button onClick ={handleArrClick}>Set Array State</button> <button onClick ={handleObjClick}>Set Object State</button> </div> ) } export default StateExample

在上面的代碼中,我們創建了兩個狀態arrobj ,並將它們分別初始化為一些數組和對象值。 然後我們創建了名為handleArrClickhandleObjClickonClick處理程序來分別設置數組和對象的狀態。 當handleArrClick觸發時,我們調用setArr並使用 ES6 擴展運算符來擴展已經存在的數組值並將newArr添加到它。

我們對handleObjClick處理程序做了同樣的事情。 這裡我們調用setObj ,使用 ES6 擴展運算符擴展現有對象值,並更新nameage的值。

useState的異步性質

正如我們已經看到的,我們通過將新值傳遞給更新函數來使用useState設置狀態。 如果更新程序被多次調用,新值將被添加到隊列中,並使用 JavaScript Object.is比較相應地完成重新渲染。

狀態是異步更新的。 這意味著首先將新狀態添加到待處理狀態,然後更新狀態。 因此,如果您立即訪問設置的狀態,您可能仍會獲得舊的狀態值。

讓我們考慮以下示例來觀察此行為。

在上面的代碼中,我們使用useState鉤子創建了一個count狀態。 然後,我們創建了一個onClick處理程序,以在單擊按鈕時增加count狀態。 觀察到雖然count狀態增加了,如h2標籤所示,但之前的狀態仍然記錄在控制台中。 這是由於鉤子的異步性質。

如果我們希望獲得新的狀態,我們可以像處理異步函數一樣處理它。 這是一種方法。

在這裡,我們存儲了創建的newCountValue來存儲更新的計數值,然後使用更新的值設置count狀態。 然後,我們在控制台中記錄了更新後的計數值。

useEffect鉤子

useEffect是大多數項目中使用的另一個重要的 React 鉤子。 它與基於類的組件的componentDidMountcomponentWillUnmountcomponentDidUpdate生命週期方法做類似的事情。 useEffect為我們提供了編寫可能對應用程序產生副作用的命令式代碼的機會。 此類影響的示例包括日誌記錄、訂閱、突變等。

用戶可以決定useEffect何時運行,但是,如果未設置,副作用將在每次渲染或重新渲染時運行。

考慮下面的例子。

 import {useState, useEffect} from 'react' const App = () =>{ const [count, setCount] = useState(0) useEffect(() =>{ console.log(count) }) return( <div> ... </div> ) }

在上面的代碼中,我們只是在useEffect中記錄了count 。 這將在每次渲染組件後運行。

有時,我們可能希望在我們的組件中(在掛載上)運行一次鉤子。 我們可以通過向useEffect鉤子提供第二個參數來實現這一點。

 import {useState, useEffect} from 'react' const App = () =>{ const [count, setCount] = useState(0) useEffect(() =>{ setCount(count + 1) }, []) return( <div> <h1>{count}</h1> ... </div> ) }

useEffect鉤子有兩個參數,第一個參數是我們要運行的函數,第二個參數是一個依賴數組。 如果未提供第二個參數,則掛鉤將連續運行。

通過向鉤子的第二個參數傳遞一個空方括號,我們指示 React 在掛載時只運行一次useEffect鉤子。 這將在h1標記中顯示值1 ,因為當組件掛載時,計數將更新一次,從 0 到 1。

我們也可以讓我們的副作用在某些依賴值發生變化時運行。 這可以通過在依賴項列表中傳遞這些值來完成。

例如,我們可以讓useEffectcount改變時運行,如下所示。

 import { useState, useEffect } from "react"; const App = () => { const [count, setCount] = useState(0); useEffect(() => { console.log(count); }, [count]); return ( <div> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }; export default App;

當滿足這兩個條件中的任何一個時,上面的useEffect就會運行。

  1. 安裝時——在組件被渲染之後。
  2. count的值發生變化時。

掛載時, console.log表達式將運行並將記錄count為 0。一旦count更新,則滿足第二個條件,因此useEffect再次運行,只要單擊按鈕,此操作就會繼續。

一旦我們為useEffect提供了第二個參數,我們就希望我們將所有依賴項傳遞給它。 如果您安裝了ESLINT ,如果任何依賴項未傳遞到參數列表,它將顯示 lint 錯誤。 這也可能使副作用表現出意外,特別是如果它取決於未傳遞的參數。

清理效果

useEffect還允許我們在組件卸載之前清理資源。 這可能是防止內存洩漏和提高應用程序效率所必需的。 為此,我們將在鉤子末尾返回清理函數。

 useEffect(() => { console.log('mounted') return () => console.log('unmounting... clean up here') })

上面的useEffect掛鉤將在組件mounted時記錄掛載。 Unmounting... clean up here將在組件卸載時記錄。 當組件從 UI 中移除時,可能會發生這種情況。

清理過程通常遵循以下表格。

 useEffect(() => { //The effect we intend to make effect //We then return the clean up return () => the cleanup/unsubscription })

雖然您可能找不到那麼多useEffect訂閱的用例,但它在處理訂閱和計時器時很有用。 特別是在處理 Web 套接字時,您可能需要在組件卸載時取消訂閱網絡以節省資源並提高性能。

使用useEffect獲取和重新獲取數據

useEffect掛鉤最常見的用例之一是從 API 獲取和預取數據。

為了說明這一點,我們將使用我從JSONPlaceholder創建的假用戶數據來使用useEffect掛鉤獲取數據。

 import { useEffect, useState } from "react"; import axios from "axios"; export default function App() { const [users, setUsers] = useState([]); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/users"; useEffect(() => { const fetchUsers = async () => { const { data } = await axios.get(endPoint); setUsers(data); }; fetchUsers(); }, []); return ( <div className="App"> {users.map((user) => ( <div> <h2>{user.name}</h2> <p>Occupation: {user.job}</p> <p>Sex: {user.sex}</p> </div> ))} </div> ); }

在上面的代碼中,我們使用useState鉤子創建了一個users狀態。 然後我們使用 Axios 從 API 獲取數據。 這是一個異步過程,所以我們使用了 async/await 函數,我們也可以使用點然後語法。 由於我們獲取了用戶列表,因此我們只需通過它進行映射以顯示數據。

請注意,我們向鉤子傳遞了一個空參數。 這確保了它在組件安裝時只被調用一次。

當某些條件發生變化時,我們也可以重新獲取數據。 我們將在下面的代碼中展示這一點。

 import { useEffect, useState } from "react"; import axios from "axios"; export default function App() { const [userIDs, setUserIDs] = useState([]); const [user, setUser] = useState({}); const [currentID, setCurrentID] = useState(1); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/userdata/users"; useEffect(() => { axios.get(endPoint).then(({ data }) => setUserIDs(data)); }, []); useEffect(() => { const fetchUserIDs = async () => { const { data } = await axios.get(`${endPoint}/${currentID}`}); setUser(data); }; fetchUserIDs(); }, [currentID]); const moveToNextUser = () => { setCurrentID((prevId) => (prevId < userIDs.length ? prevId + 1 : prevId)); }; const moveToPrevUser = () => { setCurrentID((prevId) => (prevId === 1 ? prevId : prevId - 1)); }; return ( <div className="App"> <div> <h2>{user.name}</h2> <p>Occupation: {user.job}</p> <p>Sex: {user.sex}</p> </div> <button onClick={moveToPrevUser}>Prev</button> <button onClick={moveToNextUser}>Next</button> </div> ); }

這裡我們創建了兩個useEffect鉤子。 在第一個中,我們使用點 then 語法從我們的 API 中獲取所有用戶。 這是確定用戶數量所必需的。

然後我們創建了另一個useEffect鉤子來根據id獲取用戶。 這個useEffect將在 id 更改時重新獲取數據。 為了確保這一點,我們在依賴列表中傳遞了id

接下來,我們創建了函數來在單擊按鈕時更新id的值。 一旦id的值發生變化, useEffect將再次運行並重新獲取數據。

如果我們願意,我們甚至可以在 Axios 中清理或取消基於 Promise 的令牌,我們可以使用上面討論的清理方法來做到這一點。

 useEffect(() => { const source = axios.CancelToken.source(); const fetchUsers = async () => { const { data } = await axios.get(`${endPoint}/${num}`, { cancelToken: source.token }); setUser(data); }; fetchUsers(); return () => source.cancel(); }, [num]);

在這裡,我們將 Axios 的令牌作為第二個參數傳遞給axios.get 。 當組件卸載時,我們通過調用源對象的取消方法取消訂閱。

useReducer鉤子

useReducer鉤子是一個非常有用的 React 鉤子,它與useState鉤子做類似的事情。 根據 React 文檔,這個鉤子應該用於處理比useState鉤子更複雜的邏輯。 值得注意的是, useState鉤子在內部是用 useReducer 鉤子實現的。

該鉤子將 reducer 作為參數,並且可以選擇將初始狀態和 init 函數作為參數。

 const [state, dispatch] = useReducer(reducer, initialState, init)

這裡, init是一個函數,每當我們想要懶惰地創建初始狀態時都會使用它。

讓我們看看如何通過創建一個簡單的待辦事項應用程序來實現useReducer鉤子,如下面的沙盒所示。

待辦事項示例

首先,我們應該創建我們的 reducer 來保存狀態。

 export const ADD_TODO = "ADD_TODO"; export const REMOVE_TODO = "REMOVE_TODO"; export const COMPLETE_TODO = "COMPLETE_TODO"; const reducer = (state, action) => { switch (action.type) { case ADD_TODO: const newTodo = { id: action.id, text: action.text, completed: false }; return [...state, newTodo]; case REMOVE_TODO: return state.filter((todo) => todo.id !== action.id); case COMPLETE_TODO: const completeTodo = state.map((todo) => { if (todo.id === action.id) { return { ...todo, completed: !todo.completed }; } else { return todo; } }); return completeTodo; default: return state; } }; export default reducer;

我們創建了三個與我們的動作類型相對應的常量。 我們本可以直接使用字符串,但這種方法更適合避免拼寫錯誤。

然後我們創建了 reducer 函數。 就像在Redux中一樣,reducer 必須接受 state 和 action 對象。 但與 Redux 不同的是,我們不需要在這裡初始化我們的 reducer。

此外,對於許多狀態管理用例, useReducer以及通過上下文公開的dispatch可以使更大的應用程序能夠觸發操作、更新state並監聽它。

然後我們使用switch語句來檢查用戶傳遞的動作類型。 如果操作類型是ADD_TODO ,我們要傳遞一個新的待辦事項,如果是REMOVE_TODO ,我們要過濾待辦事項並刪除與用戶傳遞的id對應的待辦事項。 如果它是COMPLETE_TODO ,我們想要映射到待辦事項並切換具有用戶傳遞的id的待辦事項。

這是我們實現reducerApp.js文件。

 import { useReducer, useState } from "react"; import "./styles.css"; import reducer, { ADD_TODO, REMOVE_TODO, COMPLETE_TODO } from "./reducer"; export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = [ { id: id, text: "First Item", completed: false } ]; //We could also pass an empty array as the initial state //const initialState = [] const [state, dispatch] = useReducer(reducer, initialState); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); dispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; const removeTodo = (id) => { dispatch({ type: REMOVE_TODO, id }); }; const completeTodo = (id) => { dispatch({ type: COMPLETE_TODO, id }); }; return ( <div className="App"> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit">+</button> </form> <div className="todos"> {state.map((todo) => ( <div key={todo.id} className="todoItem"> <p className={todo.completed && "strikethrough"}>{todo.text}</p> <span onClick={() => removeTodo(todo.id)}>✕</span> <span onClick={() => completeTodo(todo.id)}>✓</span> </div> ))} </div> </div> ); }

在這裡,我們創建了一個包含一個輸入元素的表單,用於收集用戶的輸入,以及一個用於觸發操作的按鈕。 當表單提交時,我們發送了一個ADD_TODO類型的動作,傳遞一個新的 id 和 to-do 文本。 我們通過將先前的 id 值增加 1 創建了一個新的 id。然後我們清除了輸入文本框。 要刪除並完成待辦事項,我們只需發送適當的操作即可。 如上所示,這些已經在 reducer 中實現。

然而,奇蹟發生了,因為我們使用了useReducer鉤子。 這個鉤子接受reducer和初始狀態並返回狀態和調度函數。 在這裡,dispatch 函數與useState鉤子的 setter 函數的用途相同,我們可以隨意調用它而不是dispatch

為了顯示待辦事項,我們簡單地映射到我們的狀態對像中返回的待辦事項列表,如上面的代碼所示。

這顯示了useReducer鉤子的威力。 我們也可以使用useState掛鉤來實現此功能,但正如您從上面的示例中看到的那樣, useReducer掛鉤幫助我們保持整潔。 當狀態對像是一個複雜的結構並且以不同的方式更新而不是簡單的值替換時, useReducer通常是有益的。 此外,一旦這些更新函數變得更加複雜, useReducer可以輕鬆地將所有復雜性保存在 reducer 函數(這是一個純 JS 函數)中,從而非常容易單獨為 reducer 函數編寫測試。

我們也可以將第三個參數傳遞給useReducer鉤子來懶惰地創建初始狀態。 這意味著我們可以在init函數中計算初始狀態。

例如,我們可以創建一個init函數,如下所示:

 const initFunc = () => [ { id: id, text: "First Item", completed: false } ]

然後將它傳遞給我們的useReducer鉤子。

 const [state, dispatch] = useReducer(reducer, initialState, initFunc)

如果我們這樣做, initFuncinitialState我們提供的初始狀態,初始狀態將被延遲計算。

useContext鉤子

React Context API 提供了一種在整個 React 組件樹中共享狀態或數據的方法。 該 API 已經在 React 中作為一個實驗性功能提供了一段時間,但在 React 16.3.0 中使用變得安全了。 API 使組件之間的數據共享變得容易,同時消除了螺旋鑽。

雖然您可以將 React Context 應用到整個應用程序,但也可以將其應用到應用程序的一部分。

要使用鉤子,您需要首先使用React.createContext創建一個上下文,然後可以將這個上下文傳遞給鉤子。

為了演示useContext鉤子的使用,讓我們創建一個簡單的應用程序,它將增加整個應用程序的字體大小。

讓我們在context.js文件中創建我們的上下文。

 import { createContext } from "react"; //Here, we set the initial fontSize as 16. const fontSizeContext = createContext(16); export default fontSizeContext;

在這裡,我們創建了一個上下文並傳遞了一個初始值16給它,然後導出了上下文。 接下來,讓我們將上下文連接到我們的應用程序。

 import FontSizeContext from "./context"; import { useState } from "react"; import PageOne from "./PageOne"; import PageTwo from "./PageTwo"; const App = () => { const [size, setSize] = useState(16); return ( <FontSizeContext.Provider value={size}> <PageOne /> <PageTwo /> <button onClick={() => setSize(size + 5)}>Increase font</button> <button onClick={() => setSize((prevSize) => Math.min(11, prevSize - 5)) } > Decrease font </button> </FontSizeContext.Provider> ); }; export default App;

在上面的代碼中,我們用FontSizeContext.Provider包裝了整個組件樹,並將size傳遞給它的 value 屬性。 在這裡, size是使用useState鉤子創建的狀態。 這允許我們在size狀態更改時更改 value 屬性。 通過使用Provider包裝整個組件,我們可以在應用程序的任何位置訪問上下文。

例如,我們訪問了<PageOne /><PageTwo />中的上下文。 因此,當我們從App.js文件中增加字體大小時,這兩個組件的字體大小將增加。 如上所示,我們可以通過按鈕增加或減少字體大小,一旦我們這樣做,字體大小就會在整個應用程序中發生變化。

 import { useContext } from "react"; import context from "./context"; const PageOne = () => { const size = useContext(context); return <p style={{ fontSize: `${size}px` }}>Content from the first page</p>; }; export default PageOne;

在這裡,我們使用PageOne組件中的useContext鉤子訪問上下文。 然後我們使用這個上下文來設置我們的 font-size 屬性。 類似的過程適用於PageTwo.js文件。

主題或其他高階應用程序級配置是上下文的良好候選者。

使用useContextuseReducer

當與useReducer掛鉤使用時, useContext允許我們創建自己的狀態管理系統。 我們可以創建全局狀態並在我們的應用程序中輕鬆管理它們。

讓我們使用上下文 API 改進我們的待辦事項應用程序。

像往常一樣,我們需要在todoContext.js文件中創建一個todoContext

 import { createContext } from "react"; const initialState = []; export default createContext(initialState);

在這裡,我們創建了上下文,傳遞了一個空數組的初始值。 然後我們導出上下文。

讓我們通過分離待辦事項列表和項目來重構我們的App.js文件。

 import { useReducer, useState } from "react"; import "./styles.css"; import todoReducer, { ADD_TODO } from "./todoReducer"; import TodoContext from "./todoContext"; import TodoList from "./TodoList"; export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = []; const [todoState, todoDispatch] = useReducer(todoReducer, initialState); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); todoDispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; return ( <TodoContext.Provider value={[todoState, todoDispatch]}> <div className="app"> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit"> + </button> </form> <TodoList /> </div> </TodoContext.Provider> ); }

在這裡,我們用 TodoContext.Provider 包裝了我們的App.js文件,然後我們將TodoContext.Provider的返回值todoReducer給它。 這使得 reducer 的 state 和dispatch函數可以在我們的整個應用程序中訪問。

然後,我們將待辦事項顯示分成一個組件TodoList 。 多虧了 Context API,我們在沒有道具鑽孔的情況下做到了這一點。 讓我們看一下TodoList.js文件。

 import React, { useContext } from "react"; import TodoContext from "./todoContext"; import Todo from "./Todo"; const TodoList = () => { const [state] = useContext(TodoContext); return ( <div className="todos"> {state.map((todo) => ( <Todo key={todo.id} todo={todo} /> ))} </div> ); }; export default TodoList;

使用數組解構,我們可以使用useContext鉤子從上下文中訪問狀態(離開調度函數)。 然後我們可以映射狀態並顯示待辦事項。 我們仍然在Todo組件中提取了它。 ES6+ 的 map 函數要求我們傳遞一個唯一的鍵,因為我們需要特定的 to-do,所以我們也一起傳遞它。

讓我們看一下Todo組件。

 import React, { useContext } from "react"; import TodoContext from "./todoContext"; import { REMOVE_TODO, COMPLETE_TODO } from "./todoReducer"; const Todo = ({ todo }) => { const [, dispatch] = useContext(TodoContext); const removeTodo = (id) => { dispatch({ type: REMOVE_TODO, id }); }; const completeTodo = (id) => { dispatch({ type: COMPLETE_TODO, id }); }; return ( <div className="todoItem"> <p className={todo.completed ? "strikethrough" : "nostrikes"}> {todo.text} </p> <span onClick={() => removeTodo(todo.id)}>✕</span> <span onClick={() => completeTodo(todo.id)}>✓</span> </div> ); }; export default Todo;

再次使用數組解構,我們從上下文訪問調度函數。 這允許我們定義completeTodoremoveTodo函數,正如在useReducer部分中討論的那樣。 使用從todoList.js傳遞的todo屬性,我們可以顯示一個待辦事項。 我們還可以將其標記為已完成並刪除我們認為合適的待辦事項。

也可以在我們的應用程序的根目錄中嵌套多個上下文提供程序。 這意味著我們可以使用多個上下文在應用程序中執行不同的功能。

為了演示這一點,讓我們將主題添加到待辦事項示例中。

這是我們將要構建的。

同樣,我們必須創建themeContext 。 為此,請創建一個themeContext.js文件並添加以下代碼。

 import { createContext } from "react"; import colors from "./colors"; export default createContext(colors.light);

在這裡,我們創建了一個上下文並傳遞了colors.light作為初始值。 讓我們在colors.js文件中使用此屬性定義顏色。

 const colors = { light: { backgroundColor: "#fff", color: "#000" }, dark: { backgroundColor: "#000", color: "#fff" } }; export default colors;

在上面的代碼中,我們創建了一個包含 light 和 dark 屬性的colors對象。 每個屬性都有backgroundColorcolor對象。

接下來,我們創建themeReducer來處理主題狀態。

 import Colors from "./colors"; export const LIGHT = "LIGHT"; export const DARK = "DARK"; const themeReducer = (state, action) => { switch (action.type) { case LIGHT: return { ...Colors.light }; case DARK: return { ...Colors.dark }; default: return state; } }; export default themeReducer;

像所有 reducer 一樣, themeReducer接受狀態和動作。 然後它使用switch語句來確定當前的操作。 如果它是LIGHT類型,我們只需分配Colors.light道具,如果它是DARK類型,我們顯示Colors.dark道具。 我們可以使用useState鉤子輕鬆完成此操作,但我們選擇useReducer來驅動要點。

設置好themeReducer ,我們可以將它集成到我們的App.js文件中。

 import { useReducer, useState, useCallback } from "react"; import "./styles.css"; import todoReducer, { ADD_TODO } from "./todoReducer"; import TodoContext from "./todoContext"; import ThemeContext from "./themeContext"; import TodoList from "./TodoList"; import themeReducer, { DARK, LIGHT } from "./themeReducer"; import Colors from "./colors"; import ThemeToggler from "./ThemeToggler"; const themeSetter = useCallback( theme => themeDispatch({type: theme}, [themeDispatch]); export default function App() { const [id, setId] = useState(0); const [text, setText] = useState(""); const initialState = []; const [todoState, todoDispatch] = useReducer(todoReducer, initialState); const [themeState, themeDispatch] = useReducer(themeReducer, Colors.light); const themeSetter = useCallback( (theme) => { themeDispatch({ type: theme }); }, [themeDispatch] ); const addTodoItem = (e) => { e.preventDefault(); const newId = id + 1; setId(newId); todoDispatch({ type: ADD_TODO, id: newId, text: text }); setText(""); }; return ( <TodoContext.Provider value={[todoState, todoDispatch]}> <ThemeContext.Provider value={[ themeState, themeSetter ]} > <div className="app" style={{ ...themeState }}> <ThemeToggler /> <h1>Todo Example</h1> <form className="input" onSubmit={addTodoItem}> <input value={text} onChange={(e) => setText(e.target.value)} /> <button disabled={text.length === 0} type="submit"> + </button> </form> <TodoList /> </div> </ThemeContext.Provider> </TodoContext.Provider> ); }

在上面的代碼中,我們向已經存在的待辦事項應用程序添加了一些東西。 我們首先導入ThemeContextthemeReducerThemeTogglerColors 。 We created a reducer using the useReducer hook, passing the themeReducer and an initial value of Colors.light to it. This returned the themeState and themeDispatch to us.

We then nested our component with the provider function from the ThemeContext , passing the themeState and the dispatch functions to it. We also added theme styles to it by spreading out the themeStates . This works because the colors object already defined properties similar to what the JSX styles will accept.

However, the actual theme toggling happens in the ThemeToggler component. 讓我們來看看它。

 import ThemeContext from "./themeContext"; import { useContext, useState } from "react"; import { DARK, LIGHT } from "./themeReducer"; const ThemeToggler = () => { const [showLight, setShowLight] = useState(true); const [themeState, themeSetter] = useContext(ThemeContext); const dispatchDarkTheme = () => themeSetter(DARK); const dispatchLightTheme = () => themeSetter(LIGHT); const toggleTheme = () => { showLight ? dispatchDarkTheme() : dispatchLightTheme(); setShowLight(!showLight); }; console.log(themeState); return ( <div> <button onClick={toggleTheme}> {showLight ? "Change to Dark Theme" : "Change to Light Theme"} </button> </div> ); }; export default ThemeToggler;

In this component, we used the useContext hook to retrieve the values we passed to the ThemeContext.Provider from our App.js file. As shown above, these values include the ThemeState , dispatch function for the light theme, and dispatch function for the dark theme. Thereafter, we simply called the dispatch functions to toggle the themes. We also created a state showLight to determine the current theme. This allows us to easily change the button text depending on the current theme.

The useMemo Hook

The useMemo hook is designed to memoize expensive computations. Memoization simply means caching. It caches the computation result with respect to the dependency values so that when the same values are passed, useMemo will just spit out the already computed value without recomputing it again. This can significantly improve performance when done correctly.

The hook can be used as follows:

 const memoizedResult = useMemo(() => expensiveComputation(a, b), [a, b])

Let's consider three cases of the useMemo hook.

  1. When the dependency values, a and b remain the same.
    The useMemo hook will return the already computed memoized value without recomputation.
  2. When the dependency values, a and b change.
    The hook will recompute the value.
  3. When no dependency value is passed.
    The hook will recompute the value.

Let's take a look at an example to demonstrate this concept.

In the example below, we'll be computing the PAYE and Income after PAYE of a company's employees with fake data from JSONPlaceholder.

The calculation will be based on the personal income tax calculation procedure for Nigeria providers by PricewaterhouseCoopers available here.

This is shown in the sandbox below.

First, we queried the API to get the employees' data. We also get data for each employee (with respect to their employee id).

const [employee, setEmployee] = useState({}); const [employees, setEmployees] = useState([]); const [num, setNum] = useState(1); const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/employees"; useEffect(() => { const getEmployee = async () => { const { data } = await axios.get(`${endPoint}/${num}`); setEmployee(data); }; getEmployee(); }, [num]); useEffect(() => { axios.get(endPoint).then(({ data }) => setEmployees(data)); }, [num]);

我們在第一個useEffect中使用了axiosasync/await方法,然後在第二個中使用了點 then 語法。 這兩種方法的工作方式相同。

接下來,使用我們從上面得到的員工數據,讓我們計算救濟變量:

 const taxVariablesCompute = useMemo(() => { const { income, noOfChildren, noOfDependentRelatives } = employee; //supposedly complex calculation //tax relief computations for relief Allowance, children relief, // relatives relief and pension relief const reliefs = reliefAllowance1 + reliefAllowance2 + childrenRelief + relativesRelief + pensionRelief; return reliefs; }, [employee]);

這是一個相當複雜的計算,因此我們必須將其包裝在useMemo掛鉤中以對其進行記憶或優化。 以這種方式記憶它將確保如果我們再次嘗試訪問同一員工,則不會重新計算計算。

此外,使用上面獲得的稅收減免值,我們想計算 PAYE 和 PAYE 後的收入。

 const taxCalculation = useMemo(() => { const { income } = employee; let taxableIncome = income - taxVariablesCompute; let PAYE = 0; //supposedly complex calculation //computation to compute the PAYE based on the taxable income and tax endpoints const netIncome = income - PAYE; return { PAYE, netIncome }; }, [employee, taxVariablesCompute]);

我們使用上述計算的稅收變量執行稅收計算(相當複雜的計算),然後使用useMemo掛鉤對其進行記憶。

完整的代碼可在此處獲得。

這遵循此處給出的稅收計算程序。 我們首先根據收入、子女人數和受撫養親屬人數計算稅收減免。 然後,我們逐步將應納稅所得額乘以 PIT 稅率。 雖然本教程並不完全需要計算問題,但提供它是為了向我們展示為什麼useMemo可能是必要的。 這也是一個相當複雜的計算,因此我們可能需要使用useMemo來記住它,如上所示。

計算值後,我們簡單地顯示結果。

請注意以下有關useMemo掛鉤的內容。

  • 僅當需要優化計算時才應使用useMemo 。 換句話說,當重新計算代價高昂時。
  • 建議先編寫計算而不記憶,僅在導致性能問題時才記憶。
  • 不必要和不相關的useMemo鉤子的使用甚至可能使性能問題更加複雜。
  • 有時,過多的記憶也會導致性能問題。

useCallback鉤子

useCallback的用途與useMemo相同,但它返回一個記憶化的回調而不是一個記憶化的值。 換句話說, useCallback與在沒有函數調用的情況下傳遞useMemo相同。

例如,考慮下面的代碼。

 import React, {useCallback, useMemo} from 'react' const MemoizationExample = () => { const a = 5 const b = 7 const memoResult = useMemo(() => a + b, [a, b]) const callbackResult = useCallback(a + b, [a, b]) console.log(memoResult) console.log(callbackResult) return( <div> ... </div> ) } export default MemoizationExample

在上面的示例中, memoResultcallbackResult都將給出相同的值12 。 在這裡, useCallback將返回一個記憶值。 但是,我們也可以通過將其作為函數傳遞來使其返回一個記憶化的回調。

下面的useCallback將返回一個記憶化的回調。

 ... const callbackResult = useCallback(() => a + b, [a, b]) ...

然後,我們可以在執行操作時或在useEffect掛鉤中觸發回調。

 import {useCallback, useEffect} from 'react' const memoizationExample = () => { const a = 5 const b = 7 const callbackResult = useCallback(() => a + b, [a, b]) useEffect(() => { const callback = callbackResult() console.log(callback) }) return ( <div> <button onClick= {() => console.log(callbackResult())}> Trigger Callback </button> </div> ) } export default memoizationExample

在上面的代碼中,我們使用useCallback鉤子定義了一個回調函數。 然後,當組件掛載以及單擊按鈕時,我們在useEffect掛鉤中調用回調。

useEffect和按鈕單擊都產生相同的結果。

請注意,適用於useCallback useMemo 。 我們可以使用useCallback重新創建useMemo示例。

useRef掛鉤

useRef返回一個可以在應用程序中持久存在的對象。 鉤子只有一個屬性current ,我們可以很容易地向它傳遞一個參數。

它的用途與在基於類的組件中使用的createRef相同。 我們可以使用這個鉤子創建一個引用,如下所示:

 const newRef = useRef('')

在這裡,我們創建了一個名為newRef的新 ref,並向其傳遞了一個空字符串。

這個鉤子主要用於兩個目的:

  1. 訪問或操作 DOM,以及
  2. 存儲可變狀態——當我們不希望組件在值更改時重新呈現時,這很有用。

操作 DOM

當傳遞給 DOM 元素時,ref 對象指向該元素,並可用於訪問其 DOM 屬性和屬性。

這是一個非常簡單的例子來演示這個概念。

 import React, {useRef, useEffect} from 'react' const RefExample = () => { const headingRef = useRef('') console.log(headingRef) return( <div> <h1 className='topheading' ref={headingRef}>This is a h1 element</h1> </div> ) } export default RefExample

在上面的示例中,我們使用傳遞空字符串的useRef掛鉤定義了headingRef 。 然後我們通過傳遞ref = {headingRef}h1標籤中設置 ref。 通過設置這個 ref,我們要求headingRef指向我們的h1元素。 這意味著我們可以從 ref 訪問我們的h1元素的屬性。

要看到這一點,如果我們檢查console.log(headingRef)的值,我們將得到{current: HTMLHeadingElement}{current: h1}並且我們可以評估元素的所有屬性或屬性。 類似的事情適用於任何其他 HTML 元素。

例如,我們可以在組件掛載時將文本設置為斜體。

 useEffect(() => { headingRef.current.style.font; }, []);

我們甚至可以將文本更改為其他內容。

 ... headingRef.current.innerHTML = "A Changed H1 Element"; ...

我們甚至可以更改父容器的背景顏色。

 ... headingRef.current.parentNode.style.backgroundColor = "red"; ...

任何類型的 DOM 操作都可以在這裡完成。 請注意, headingRef.current的讀取方式與document.querySelector('.topheading')相同。

useRef鉤子在操作 DOM 元素時的一個有趣用例是將光標聚焦在輸入元素上。 讓我們快速瀏覽一下。

 import {useRef, useEffect} from 'react' const inputRefExample = () => { const inputRef = useRef(null) useEffect(() => { inputRef.current.focus() }, []) return( <div> <input ref={inputRef} /> <button onClick = {() => inputRef.current.focus()}>Focus on Input </button> </div> ) } export default inputRefExample

在上面的代碼中,我們使用useRef鉤子創建了inputRef ,然後讓它指向輸入元素。 然後,當組件加載和使用inputRef.current.focus()單擊按鈕時,我們將光標聚焦在輸入引用上。 這是可能的,因為focus()是輸入元素的屬性,因此 ref 將能夠評估方法。

可以通過使用React.forwardRef()轉發子組件來評估在父組件中創建的 Refs。 讓我們來看看它。

讓我們首先創建另一個組件NewInput.js並在其中添加以下代碼。

 import { useRef, forwardRef } from "react"; const NewInput = forwardRef((props, ref) => { return <input placeholder={props.val} ref={ref} />; }); export default NewInput;

該組件接受propsref 。 我們將 ref 傳遞給它的 ref prop,並將props.val給它的 placeholder prop。 常規 React 組件不採用ref屬性。 這個屬性只有當我們用React.forwardRef包裝它時才可用,如上所示。

然後我們可以輕鬆地在父組件中調用它。

 ... <NewInput val="Just an example" ref={inputRef} /> ...

存儲可變狀態

Refs 不僅用於操作 DOM 元素,還可以用於存儲可變值,而無需重新渲染整個組件。

以下示例將檢測在不重新渲染組件的情況下單擊按鈕的次數。

 import { useRef } from "react"; export default function App() { const countRef = useRef(0); const increment = () => { countRef.current++; console.log(countRef); }; return ( <div className="App"> <button onClick={increment}>Increment </button> </div> ); }

在上面的代碼中,我們在單擊按鈕時遞增countRef ,然後將其記錄到控制台。 儘管控制台中顯示的值增加了,但如果我們嘗試直接在我們的組件中評估它,我們將無法看到任何變化。 它只會在重新渲染時在組件中更新。

請注意,雖然useState是異步的, useRef是同步的。 換句話說,該值在更新後立即可用。

useLayoutEffect鉤子

useEffect掛鉤一樣,在安裝和渲染組件後調用useLayoutEffect 。 這個鉤子在 DOM 突變後觸發,並且是同步的。 除了在 DOM 突變後被同步調用之外, useEffectuseLayoutEffect做同樣的事情。

useLayoutEffect只能用於執行 DOM 突變或 DOM 相關的測量,否則,您應該使用useEffect掛鉤。 對 DOM 突變函數使用useEffect鉤子可能會導致一些性能問題,例如閃爍,但useLayoutEffect可以完美地處理它們,因為它在發生突變後運行。

讓我們看一些例子來演示這個概念。

  1. 我們將在調整大小時獲得窗口的寬度和高度。
 import {useState, useLayoutEffect} from 'react' const ResizeExample = () =>{ const [windowSize, setWindowSize] = useState({width: 0, height: 0}) useLayoutEffect(() => { const resizeWindow = () => setWindowSize({ width: window.innerWidth, height: window.innerHeight }) window.addEventListener('resize', resizeWindow) return () => window.removeEventListener('resize', resizeWindow) }, []) return ( <div> <p>width: {windowSize.width}</p> <p>height: {windowSize.height}</p> </div> ) } export default ResizeExample

在上面的代碼中,我們創建了一個具有寬度和高度屬性的狀態windowSize 。 然後我們在調整窗口大小時將狀態分別設置為當前窗口的寬度和高度。 我們還清理了卸載時的代碼。 清理過程在useLayoutEffect中是必不可少的,用於清理 DOM 操作並提高效率。

  1. 讓我們用useLayoutEffect模糊文本。
 import { useRef, useState, useLayoutEffect } from "react"; export default function App() { const paragraphRef = useRef(""); useLayoutEffect(() => { const { current } = paragraphRef; const blurredEffect = () => { current.style.color = "transparent"; current.style.textShadow = "0 0 5px rgba(0,0,0,0.5)"; }; current.addEventListener("click", blurredEffect); return () => current.removeEventListener("click", blurredEffect); }, []); return ( <div className="App"> <p ref={paragraphRef}>This is the text to blur</p> </div> ); }

我們在上面的代碼中一起使用了useRefuseLayoutEffect 。 我們首先創建了一個引用, paragraphRef來指向我們的段落。 然後我們創建了一個點擊事件監聽器來監控段落何時被點擊,然後使用我們定義的樣式屬性對其進行模糊處理。 最後,我們使用removeEventListener清理了事件監聽器。

useDispatchuseSelector Hooks

useDispatch是一個 Redux 鉤子,用於在應用程序中分派(觸發)操作。 它將動作對像作為參數並調用動作。 useDispatch是鉤子的等價物mapDispatchToProps

另一方面, useSelector是一個用於評估 Redux 狀態的 Redux 鉤子。 它需要一個函數從 store 中選擇確切的 Redux reducer,然後返回相應的狀態。

一旦我們的 Redux 存儲通過 Redux 提供程序連接到 React 應用程序,我們就可以使用useSelector調用操作並使用useDispatch訪問狀態。 每個 Redux 動作和狀態都可以使用這兩個鉤子進行評估。

請注意,這些狀態隨 React Redux(一個可以在 React 應用程序中輕鬆評估 Redux 存儲的包)一起提供。 它們在核心 Redux 庫中不可用。

這些鉤子使用起來非常簡單。 首先,我們必須聲明調度函數,然後觸發它。

 import {useDispatch, useSelector} from 'react-redux' import {useEffect} from 'react' const myaction from '...' const ReduxHooksExample = () =>{ const dispatch = useDispatch() useEffect(() => { dispatch(myaction()); //alternatively, we can do this dispatch({type: 'MY_ACTION_TYPE'}) }, []) const mystate = useSelector(state => state.myReducerstate) return( ... ) } export default ReduxHooksExample

在上面的代碼中,我們從react-redux導入useDispatchuseSelector 。 然後,在一個useEffect鉤子中,我們調度了這個動作。 我們可以在另一個文件中定義動作,然後在這裡調用它,或者我們可以直接定義它,如useEffect調用中所示。

一旦我們發送了動作,我們的狀態將可用。 然後我們可以使用useSelector鉤子檢索狀態,如圖所示。 可以像使用useState鉤子中的狀態一樣使用狀態。

讓我們看一個例子來演示這兩個鉤子。

為了演示這個概念,我們必須創建一個 Redux 存儲、reducer 和操作。 為了簡化這裡的事情,我們將使用 Redux Toolkit 庫和來自 JSONPlaceholder 的假數據庫。

我們需要安裝以下軟件包才能開始。 運行以下 bash 命令。

 npm i redux @reduxjs/toolkit react-redux axios

首先,讓我們創建employeesSlice.js來處理我們員工API 的reducer 和action。

 import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import axios from "axios"; const endPoint = "https://my-json-server.typicode.com/ifeanyidike/jsondata/employees"; export const fetchEmployees = createAsyncThunk("employees/fetchAll", async () => { const { data } = await axios.get(endPoint); return data; }); const employeesSlice = createSlice({ name: "employees", initialState: { employees: [], loading: false, error: "" }, reducers: {}, extraReducers: { [fetchEmployees.pending]: (state, action) => { state.status = "loading"; }, [fetchEmployees.fulfilled]: (state, action) => { state.status = "success"; state.employees = action.payload; }, [fetchEmployees.rejected]: (state, action) => { state.status = "error"; state.error = action.error.message; } } }); export default employeesSlice.reducer;

這是 Redux 工具包的標准設置。 我們使用createAsyncThunk訪問Thunk中間件以執行異步操作。 這使我們能夠從 API 中獲取員工列表。 然後,我們創建了employeesSlice並根據操作類型返回“加載”、“錯誤”和員工數據。

Redux 工具包還使設置商店變得容易。 這裡是商店。

 import { configureStore } from "@reduxjs/toolkit"; import { combineReducers } from "redux"; import employeesReducer from "./employeesSlice"; const reducer = combineReducers({ employees: employeesReducer }); export default configureStore({ reducer });;

在這裡,我們使用combineReducers來捆綁 reducer 和 Redux 工具包提供的configureStore函數來設置 store。

讓我們繼續在我們的應用程序中使用它。

首先,我們需要將 Redux 連接到我們的 React 應用程序。 理想情況下,這應該在我們應用程序的根目錄下完成。 我喜歡在index.js文件中執行此操作。

 import React, { StrictMode } from "react"; import ReactDOM from "react-dom"; import store from "./redux/store"; import { Provider } from "react-redux"; import App from "./App"; const rootElement = document.getElementById("root"); ReactDOM.render( <Provider store={store}> <StrictMode> <App /> </StrictMode> </Provider>, rootElement );

在這裡,我已經導入了我在上面創建的商店以及來自react-redux Provider

然後,我用Provider函數包裝了整個應用程序,將 store 傳遞給它。 這使得商店可以在我們的整個應用程序中訪問。

然後我們可以繼續使用useDispatchuseSelector掛鉤來獲取數據。

讓我們在App.js文件中執行此操作。

 import { useDispatch, useSelector } from "react-redux"; import { fetchEmployees } from "./redux/employeesSlice"; import { useEffect } from "react"; export default function App() { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchEmployees()); }, [dispatch]); const employeesState = useSelector((state) => state.employees); const { employees, loading, error } = employeesState; return ( <div className="App"> {loading ? ( "Loading..." ) : error ? ( <div>{error}</div> ) : ( <> <h1>List of Employees</h1> {employees.map((employee) => ( <div key={employee.id}> <h3>{`${employee.firstName} ${employee.lastName}`}</h3> </div> ))} </> )} </div> ); }

在上面的代碼中,我們使用useDispatch掛鉤來調用在employeesSlice.js文件中創建的fetchEmployees操作。 這使得員工狀態在我們的應用程序中可用。 然後,我們使用useSelector鉤子來獲取狀態。 此後,我們通過employees映射顯示結果。

useHistory鉤子

導航在 React 應用程序中非常重要。 雖然您可以通過多種方式實現這一點,但 React Router 提供了一種簡單、高效且流行的方式來實現 React 應用程序中的動態路由。 此外,React Router 提供了幾個鉤子來評估路由器的狀態並在瀏覽器上執行導航,但是要使用它們,您需要首先正確設置您的應用程序。

要使用任何 React Router 鉤子,我們應該首先用BrowserRouter包裝我們的應用程序。 然後我們可以使用SwitchRoute嵌套路由。

但首先,我們必須通過運行以下命令來安裝包。

 npm install react-router-dom

然後,我們需要如下設置我們的應用程序。 我喜歡在我的App.js文件中執行此操作。

 import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import Employees from "./components/Employees"; export default function App() { return ( <div className="App"> <Router> <Switch> <Route path='/'> <Employees /> </Route> ... </Switch> </Router> </div> ); }

根據我們希望渲染的組件數量,我們可以擁有盡可能多的路由。 在這裡,我們只渲染了Employees組件。 path屬性告訴 React Router DOM 組件的路徑,並且可以使用查詢字符串或各種其他方法進行評估。

順序在這裡很重要。 根路由應放置在子路由下方,依此類推。 要覆蓋此順序,您需要在根路由中包含exact關鍵字。

 <Route path='/' exact > <Employees /> </Route>

現在我們已經設置了路由器,我們可以在我們的應用程序中使用useHistory鉤子和其他 React Router 鉤子。

要使用useHistory鉤子,我們需要先聲明如下。

 import {useHistory} from 'history' import {useHistory} from 'react-router-dom' const Employees = () =>{ const history = useHistory() ... }

如果我們將歷史記錄到控制台,我們將看到與其關聯的幾個屬性。 這些包括blockcreateHrefgogoBackgoForwardlengthlistenlocationpushreplace 。 雖然所有這些屬性都很有用,但您很可能會比其他屬性更頻繁地使用history.pushhistory.replace

讓我們使用這個屬性從一個頁面移動到另一個頁面。

假設我們想在點擊某個員工的名字時獲取他們的數據。 我們可以使用useHistory掛鉤導航到將顯示員工信息的新頁面。

 function moveToPage = (id) =>{ history.push(`/employees/${id}`) }

我們可以通過添加以下內容在Employee.js文件中實現這一點。

 import { useEffect } from "react"; import { Link, useHistory, useLocation } from "react-router-dom"; export default function Employees() { const history = useHistory(); function pushToPage = (id) => { history.push(`/employees/${id}`) } ... return ( <div> ... <h1>List of Employees</h1> {employees.map((employee) => ( <div key={employee.id}> <span>{`${employee.firstName} ${employee.lastName} `}</span> <button onClick={pushToPage(employee.id)}> » </button> </div> ))} </div> ); }

pushToPage函數中,我們使用useHistory鉤子中的history來導航到員工頁面,並在旁邊傳遞員工 ID。

useLocation掛鉤

這個鉤子也隨 React Router DOM 一起提供。 這是一個非常流行的鉤子,用於處理查詢字符串參數。 這個鉤子類似於瀏覽器中的window.location

 import {useLocation} from 'react' const LocationExample = () =>{ const location = useLocation() return ( ... ) } export default LocationExample

useLocation掛鉤返迴pathnamesearch參數、 hashstate 。 最常用的參數包括pathnamesearch ,但您同樣可以使用hash ,並在應用程序中state很多內容。

location pathname屬性將返回我們在Route設置中設置的路徑。 而search將返回查詢搜索參數(如果有)。 例如,如果我們將'http://mywebsite.com/employee/?id=1'傳遞給我們的查詢,則pathname/employee並且search將是?id=1

然後,我們可以使用查詢字符串等包或對它們進行編碼來檢索各種搜索參數。

useParams鉤子

如果我們在路徑屬性中使用 URL 參數設置 Route,我們可以使用useParams掛鉤將這些參數評估為鍵/值對。

例如,假設我們有以下路線。

 <Route path='/employees/:id' > <Employees /> </Route>

Route 將需要一個動態 id 來代替:id

使用useParams鉤子,我們可以評估用戶傳遞的 id,如果有的話。

例如,假設用戶使用history.push傳遞以下函數,

 function goToPage = () => { history.push(`/employee/3`) }

我們可以使用useParams鉤子來訪問這個 URL 參數,如下所示。

 import {useParams} from 'react-router-dom' const ParamsExample = () =>{ const params = useParams() console.log(params) return( <div> ... </div> ) } export default ParamsExample

如果我們將params記錄到控制台,我們將獲得以下對象{id: "3"}

useRouteMatch鉤子

這個鉤子提供了對匹配對象的訪問。 如果沒有提供參數,它會返回與組件最接近的匹配項。

match 對象返回幾個參數,包括path (與 Route 中指定的路徑相同)、 URLparams對象和isExact

例如,我們可以使用useRouteMatch根據路由返回組件。

 import { useRouteMatch } from "react-router-dom"; import Employees from "..."; import Admin from "..." const CustomRoute = () => { const match = useRouteMatch("/employees/:id"); return match ? ( <Employee /> ) : ( <Admin /> ); }; export default CustomRoute;

在上面的代碼中,我們使用useRouteMatch設置了一個路由的路徑,然後根據用戶選擇的路由渲染<Employee /><Admin />組件。

為此,我們仍然需要將路由添加到我們的App.js文件中。

 ... <Route> <CustomRoute /> </Route> ...

構建自定義鉤子

根據 React 文檔,構建自定義鉤子允許我們將邏輯提取到可重用函數中。 但是,您需要確保適用於 React 掛鉤的所有規則都適用於您的自定義掛鉤。 檢查本教程頂部的 React 鉤子規則,並確保您的自定義鉤子符合每個規則。

自定義鉤子允許我們編寫一次函數並在需要時重用它們,因此遵守 DRY 原則。

例如,我們可以創建一個自定義鉤子來獲取頁面上的滾動位置,如下所示。

 import { useLayoutEffect, useState } from "react"; export const useScrollPos = () => { const [scrollPos, setScrollPos] = useState({ x: 0, y: 0 }); useLayoutEffect(() => { const getScrollPos = () => setScrollPos({ x: window.pageXOffset, y: window.pageYOffset }); window.addEventListener("scroll", getScrollPos); return () => window.removeEventListener("scroll", getScrollPos); }, []); return scrollPos; };

在這裡,我們定義了一個自定義鉤子來確定頁面上的滾動位置。 為此,我們首先創建了一個狀態scrollPos來存儲滾動位置。 由於這將修改 DOM,我們需要使用useLayoutEffect而不是useEffect 。 我們添加了一個滾動事件偵聽器來捕獲 x 和 y 滾動位置,然後清理事件偵聽器。 最後,我們回到了滾動位置。

我們可以在應用程序的任何地方使用這個自定義鉤子,方法是調用它並使用它,就像我們使用任何其他狀態一樣。

 import {useScrollPos} from './Scroll' const App = () =>{ const scrollPos = useScrollPos() console.log(scrollPos.x, scrollPos.y) return ( ... ) } export default App

在這裡,我們導入了我們在上面創建的自定義鉤子useScrollPos 。 然後我們初始化它,然後將值記錄到我們的控制台。 如果我們在頁面上滾動,鉤子會在滾動的每一步向我們顯示滾動位置。

我們可以創建自定義掛鉤來執行我們在應用程序中可以想像的任何事情。 正如你所看到的,我們只需要使用內置的 React 鉤子來執行一些功能。 我們也可以使用第三方庫來創建自定義掛鉤,但如果這樣做,我們必須安裝該庫才能使用掛鉤。

結論

在本教程中,我們很好地了解了您將在大多數應用程序中使用的一些有用的 React 鉤子。 我們檢查了它們呈現的內容以及如何在您的應用程序中使用它們。 我們還查看了幾個代碼示例,以幫助您理解這些鉤子並將它們應用到您的應用程序中。

我鼓勵您在自己的應用程序中嘗試這些鉤子以進一步了解它們。

React 文檔中的資源

  • 鉤子常見問題
  • Redux 工具包
  • 使用狀態鉤子
  • 使用效果掛鉤
  • 掛鉤 API 參考
  • React Redux 鉤子
  • 反應路由器鉤子