可以在项目中使用的有用的 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 钩子
  • 反应路由器钩子