可以在项目中使用的有用的 React Hooks
已发表: 2022-03-10Hooks 是简单的函数,允许你连接或使用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)
其中mapStateToProps
和mapDispatchToProps
是要定义的函数。
而在 Hooks 时代,使用 Redux 的useSelector
和useDispatch
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 公约和规则
在深入研究各种钩子之前,看看适用于它们的约定和规则可能会有所帮助。 以下是适用于钩子的一些规则。
- 挂钩的命名约定应以前缀
use
开头。 因此,我们可以有useState
、useEffect
等。如果您使用的是 Atom 和 VSCode 等现代代码编辑器,ESLint 插件可能是 React hooks 的一个非常有用的功能。 该插件提供了有关最佳实践的有用警告和提示。 - 钩子必须在组件的顶层调用,在 return 语句之前。 它们不能在条件语句、循环或嵌套函数中调用。
- 必须从 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
钩子返回一个数组,也称为状态变量,在上面的示例中,我们将数组解构为state
和updater
函数。
重新渲染组件
使用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
在上面的代码中,我们创建了两个状态arr
和obj
,并将它们分别初始化为一些数组和对象值。 然后我们创建了名为handleArrClick
和handleObjClick
的onClick
处理程序来分别设置数组和对象的状态。 当handleArrClick
触发时,我们调用setArr
并使用 ES6 扩展运算符来扩展已经存在的数组值并将newArr
添加到它。
我们对handleObjClick
处理程序做了同样的事情。 这里我们调用setObj
,使用 ES6 扩展运算符扩展现有对象值,并更新name
和age
的值。
useState
的异步性质
正如我们已经看到的,我们通过将新值传递给更新函数来使用useState
设置状态。 如果更新程序被多次调用,新值将被添加到队列中,并使用 JavaScript Object.is
比较相应地完成重新渲染。
状态是异步更新的。 这意味着首先将新状态添加到待处理状态,然后更新状态。 因此,如果您立即访问设置的状态,您可能仍会获得旧的状态值。
让我们考虑以下示例来观察此行为。
在上面的代码中,我们使用useState
钩子创建了一个count
状态。 然后,我们创建了一个onClick
处理程序,以在单击按钮时增加count
状态。 观察到虽然count
状态增加了,如h2
标签所示,但之前的状态仍然记录在控制台中。 这是由于钩子的异步性质。
如果我们希望获得新的状态,我们可以像处理异步函数一样处理它。 这是一种方法。
在这里,我们存储了创建的newCountValue
来存储更新的计数值,然后使用更新的值设置count
状态。 然后,我们在控制台中记录了更新后的计数值。
useEffect
钩子
useEffect
是大多数项目中使用的另一个重要的 React 钩子。 它与基于类的组件的componentDidMount
、 componentWillUnmount
和componentDidUpdate
生命周期方法做类似的事情。 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。
我们也可以让我们的副作用在某些依赖值发生变化时运行。 这可以通过在依赖项列表中传递这些值来完成。
例如,我们可以让useEffect
在count
改变时运行,如下所示。
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
就会运行。
- 安装时——在组件被渲染之后。
- 当
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
的待办事项。
这是我们实现reducer
的App.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)
如果我们这样做, initFunc
将initialState
我们提供的初始状态,初始状态将被延迟计算。
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
文件。
主题或其他高阶应用程序级配置是上下文的良好候选者。
使用useContext
和useReducer
当与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;
再次使用数组解构,我们从上下文访问调度函数。 这允许我们定义completeTodo
和removeTodo
函数,正如在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
对象。 每个属性都有backgroundColor
和color
对象。
接下来,我们创建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> ); }
在上面的代码中,我们向已经存在的待办事项应用程序添加了一些东西。 我们首先导入ThemeContext
、 themeReducer
、 ThemeToggler
和Colors
。 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.
- When the dependency values, a and b remain the same.
TheuseMemo
hook will return the already computed memoized value without recomputation. - When the dependency values, a and b change.
The hook will recompute the value. - 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
中使用了axios
和async/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
在上面的示例中, memoResult
和callbackResult
都将给出相同的值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,并向其传递了一个空字符串。
这个钩子主要用于两个目的:
- 访问或操作 DOM,以及
- 存储可变状态——当我们不希望组件在值更改时重新呈现时,这很有用。
操作 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;
该组件接受props
和ref
。 我们将 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 突变后被同步调用之外, useEffect
与useLayoutEffect
做同样的事情。
useLayoutEffect
只能用于执行 DOM 突变或 DOM 相关的测量,否则,您应该使用useEffect
挂钩。 对 DOM 突变函数使用useEffect
钩子可能会导致一些性能问题,例如闪烁,但useLayoutEffect
可以完美地处理它们,因为它在发生突变后运行。
让我们看一些例子来演示这个概念。
- 我们将在调整大小时获得窗口的宽度和高度。
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 操作并提高效率。
- 让我们用
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> ); }
我们在上面的代码中一起使用了useRef
和useLayoutEffect
。 我们首先创建了一个引用, paragraphRef
来指向我们的段落。 然后我们创建了一个点击事件监听器来监控段落何时被点击,然后使用我们定义的样式属性对其进行模糊处理。 最后,我们使用removeEventListener
清理了事件监听器。
useDispatch
和useSelector
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
导入useDispatch
和useSelector
。 然后,在一个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 传递给它。 这使得商店可以在我们的整个应用程序中访问。
然后我们可以继续使用useDispatch
和useSelector
挂钩来获取数据。
让我们在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
包装我们的应用程序。 然后我们可以使用Switch
和Route
嵌套路由。
但首先,我们必须通过运行以下命令来安装包。
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() ... }
如果我们将历史记录到控制台,我们将看到与其关联的几个属性。 这些包括block
、 createHref
、 go
、 goBack
、 goForward
、 length
、 listen
、 location
、 push
、 replace
。 虽然所有这些属性都很有用,但您很可能会比其他属性更频繁地使用history.push
和history.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
挂钩返回pathname
、 search
参数、 hash
和state
。 最常用的参数包括pathname
和search
,但您同样可以使用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 中指定的路径相同)、 URL
、 params
对象和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 钩子
- 反应路由器钩子