React Context 和 Hooks:一个了解其工作原理的开源项目
中级文章
关于学习新事物的最佳方法有很多,其中之一就是实践。我同意这种方法,只要你已经掌握了基础知识,并且有一个通用的思维模型,能够为你提供关于所学内容的正确背景。
例如,如果您要学习如何从 React API 中使用Context和Hooks,您需要熟悉以下主题,否则您将完全迷失:
- 功能组件
- React 生命周期事件
- JavaScript 中的状态和状态管理的概念
- Hook 的概念
- JavaScript 的上下文和范围概念
- DOM
- JavaScript 现代特性
如果您对上述主题感到满意,请继续阅读;否则,您可以稍后再回来阅读。
这次,我想与大家分享我使用Context React 对象和 React Hooks从头开始构建 React App 的经验,其中不包含类组件,只包含功能组件。
项目
一个简单的博客,前端有一个 React 应用,允许你搜索和阅读博客文章(基于React 的Context和Hooks功能构建)。文章数据由 NodeJS 构建的后端应用程序通过 API 调用获取。
您可以在此处找到开源项目。
目标
我在这个项目中的目标是创建一个简单的 Web 应用程序,为那些难以掌握使用 React Context对象和钩子构建 React 应用程序的概念和实际方面的人提供参考。
应用程序架构
前端
前端是一个使用Context、Hooks和Functional Components构建的 React App 。
请记住,Context对象是一个 JavaScript 对象,它允许您管理应用程序的状态(数据)。在这个项目中,我们有一个Context对象,它帮助我们处理从后端(Context.js)获取的文章数据,以及另一个Context 对象,它帮助我们处理应该提供给某些组件的文章,以便在搜索请求后显示给用户(SearchContext.js)。
后端
后端使用 NodeJS 和 Express 构建。它的唯一目的是提供一个端点,以便在客户端(在本例中是来自 React App)发出请求时以 JSON 格式提供文章数据。
数据
在这个版本中,我没有添加任何数据库,而是使用文件系统来保存文章。为什么?因为这个项目的重点主要在前端,而这种存储数据的方式足以让我们的 NodeJS API 正常工作。
为什么要使用 Context 和 Hooks
React API 的这些新功能各有优缺点。不过,以下是我在这个项目中发现最相关的功能:
-
优点:使用Context可以将数据传递给应用中的任何组件,而无需手动将数据传递到 DOM 树的每一层。对于这个特定的项目,Context功能允许我在单个组件(上下文提供程序)中管理博客文章的状态,该组件可以导入到任何其他组件中,以便使其能够访问先前通过 API 调用从后端检索的数据。
-
缺点:目前,使用Jest测试使用Context提供程序数据的组件比使用传统方式测试更困难。另一方面,使用Hooks管理应用程序数据的状态比使用Class Component的传统生命周期方法更“神奇” 。
React Hooks 与传统生命周期方法
我假设你熟悉React 的componentDidMount
、componentDidUpdate
和其他生命周期方法。简而言之,为了学习方便,一些Hook允许你执行与生命周期方法相同的操作,但在函数组件中,无需编写类组件来初始化和处理组件的状态。
让我们看一个项目中使用useState()和useEffect React Hooks 的示例。检查以下代码,包括注释代码,它解释了每一行代码的用途:
// Context.js
import React, { useState, useEffect } from "react"; // imports React, and the useState and useEffect basic hooks from react library
import axios from "axios"; // imports axios from the axios package to make the API call to the back-end
const Context = React.createContext(); // creates a Context object from the React.createContext() method. You will reference this Context object when the blog posts data fetched from the NodeJS API needs to be accessible by other components at different nesting levels.
function ContextProvider() {} // Functional component definition for a component named ContextProvider. This Functional Component will be in charged of fetching the data from the back end and handle the state (blog articles) data of the application
export { ContextProvider, Context }; // export the ContextProvider functional component, and the Context object to make them available to other modules in the React app
使用上面的代码,我们创建了一个Context.js文件,它的唯一职责是允许其他组件访问从后端检索的文章数据。为此,我们需要创建一个新的Context(const Context = React.createContext()
),以及一个允许我们将该Context提供给其他组件的函数组件()。function ContextProvider( ) {}
现在我们已经有了使用我们自己的Context来处理文章状态的文件的基本结构,让我们在ContextProvider 功能组件中编写代码,它将设置初始状态并处理任何更改:
import React, { useState, useEffect } from "react";
import axios from "axios";
const Context = React.createContext();
function ContextProvider({ children }) {
const [articles, setArticles] = useState([]); // useState() hook call, that initializes the state of the articles to an empty array
useEffect(() => {
// useEffect hook call which will be invoked the first time the DOM mount. it is like using componentDidMount in Class Components
fetchArticles(); // the function that will be called as soon as the DOM mounted
}, []);
async function fetchArticles() {
// the asyncronous definition of the fetchArticles function that will retrieve the articles from the NodeJS api
try {
const content = await axios.get("/api/tutorials"); // the API call to fetch the articles from the back end
setArticles(content.data); // the setArticles function allows us to update the state of the component via the useState() hook
} catch (error) {
console.log(error);
}
}
return <Context.Provider value={{ articles }}>{children}</Context.Provider>; // the returned value from the component
}
export { ContextProvider, Context };
让我们仔细看看上面写的每一行。
ContextProvider 组件
function ContextProvider({ children }) {...}
:这是一个函数组件定义,它接受一个名为children的参数。children参数可以是任何函数组件,它们将接收此ContextProvider函数处理的状态,并且是ContextProvider组件的子组件。查看此示例。
中的花括号{children}
可能看起来有些奇怪。这是 JavaScript 的新特性允许我们解构对象或数组的方式。例如:
const fullName = { firstName: "Nayib", lastName: "Abdalá" };
const { firstName, lastName } = fullName; // JS object deconstruction
console.log(firstName); // Nayib
console.log(lastName); // Abdalá
简而言之,这const [articles, setArticles] = useState([]);
行代码帮助我们初始化并处理将从后端获取的文章的状态。让我们看看具体怎么做。
使用 useState() Hook 初始化应用程序状态
const [articles, setArticles] = useState([]);
:这行代码看起来很奇怪吗?其实很简单。const
关键字 允许我们声明一个名为 的常量articles
,以及一个名为 的常量setArticles
。分配给每个常量的值都是调用useState()
钩子的返回值,该钩子返回一个包含 2 个元素的数组,而JavaScript 的解构特性允许我们将每个元素赋值给表达式 左侧定义的每个常量const [articles, setArticles] = useState([]);
。
钩子返回的数组包含给定变量的当前状态,以及一个用于更新该状态的函数,该函数可以在函数组件useState()
中随时使用来更新该状态。在本例中,我们将 的值初始化为一个空数组(传递给函数时)。articles
[]
useState([])
您可以在此处了解有关 useState() 钩子的更多信息。
使用 useEffect() Hook 监听状态变化
useEffect(() => { ... }, [])
:
该useEffect()
钩子将在每次渲染完成后运行,但您可以将其设置为仅在某个值发生变化时运行。useEffect()
接收两个参数:一个函数,第二个参数是何时调用第一个参数函数的配置。
如果传递一个空数组作为第二个参数,则该函数应仅在第一次完成渲染时调用。如果将一个或多个变量名称作为数组元素传递给 的第二个参数,则每当这些变量的值发生变化时,都会useEffect()
调用作为第一个参数传递给 的函数。useEffect()
在我们的例子中,作为第一个参数传递给 的函数useEffect()
将仅在 DOM 第一次渲染时被调用,因为我们将一个空数组作为第二个参数传递给。您可以在此处 了解有关 useEffect() 钩子的useEffect(() => { ... }, [])
更多信息。
每次useEffect(() => { ... }, [])
调用钩子时,都会调用该函数,该函数将从该项目fetchArticles()
的后端 NodeJS API 获取文章的数据。
一旦fetchArticles()
调用,该函数体中的程序将调用函数,该函数接收从 API 获取的数据setArticles(content.data);
作为参数,并将返回的值设置为的更新值。content.data
content.date
articles
这就是useEffect()
钩子如何允许我们监听 DOM 的新渲染,并在挂载的 DOM 发生变化时或每次发生变化时执行一个操作,或者执行我们想要useEffect()
作为第二个参数传递给钩子的任何特定变量。
返回上下文提供程序,以便其他组件可以访问状态
一旦我们清楚地了解了如何处理文章的状态,我们现在需要返回所需的内容,以便我们可以将状态articles
提供给其他组件。为此,我们需要访问Provider React 组件,以便我们可以与其他组件共享在组件中初始化和处理的数据ContextProvider
。
使用 React API 函数创建每个 React ContextReact.createContext()
对象时,它都有两个组件作为方法:
- Provider方法- 提供值的组件
- 消费者方法——消费值的组件
Provider React 组件允许子组件使用Provider有权访问的任何数据。
使组件状态ContextProvider
可用的方法是返回一个Context.Provider
React 组件,并传递一个value
包含数据的 prop articles
,以便使其可供该Provider后代的任何消费组件使用。
什么?!我知道这看起来很混乱,但其实很简单。让我们分块来看代码,让它更清晰一些:
当调用<Context.Provider />
组件并将包含在value
props 中的变量传递给该Provider组件(在我们的例子中是变量)时,您将授予Providerarticles
可能包装的任何后代组件对该变量的访问权限。
如果我们将<Context.Provider />
组件记录到项目示例的控制台,您将看到以下内容:
[Click to expand] <Context.Provider />
Props: {value: {…}, children: {…}}
value: {articles: Array(2)}
...
Nodes: [div.wrapper]
不要害怕细节;您上面看到的基本上是Provider组件,它可以访问您通过value
prop 授予访问权限的数据。
总而言之,您需要从ContextProvider组件返回一个Provider组件,其中包含您需要提供给其他组件的数据:children
return <Context.Provider value={{ articles }}>{children}</Context.Provider>;
例如,<ContextProvider />
下面组件中包装的所有组件都可以访问Context数据(查看 repo 中的文件):
<ContextProvider>
/* all the children components called here will have access to the data from
the ContextProvider component */
</ContextProvider>
如果上面的内容让你感到不知所措,别担心。再读一遍。要点是,你需要将所有children
需要访问Provider数据的元素包装到Context.Provider组件中。
休息一下...
下一节与本节类似,但它解释了<ContextProviderSearch />
我创建的用于处理给定搜索的数据的组件。
使用上下文作为分离关注点和处理数据的方式
作为我们应用程序中的一个单独关注点,我们需要一个新的上下文articles
来处理在发生给定搜索查询时应该向用户显示的状态。
我将这个新的Context命名为 ContextProviderSearch。它依赖于articles
来自 的数据Context.js
。
让我们看一下SearchContext.js文件,以了解上一节中的Contextarticles
对象在本例中是如何访问的:
import React, { useState, useContext } from "react";
// code omitted
import { Context as AppContext } from "./Context"; // imports the Context provided by Context.js
const Context = React.createContext();
// code omitted
function ContextProviderSearch({ children }) {
// code omitted
const { articles } = useContext(AppContext); // Access the articles array from the Context.js file
// code omitted
return (
<Context.Provider
value={
{
/*all the props that will be required by consumer components*/
}
}
>
{/* any consumer component*/}
</Context.Provider>
);
}
export { ContextProviderSearch, Context };
对于我们的目的来说,该文件中最重要的行是import { Context as AppContext } from "./Context"
和const { articles } = useContext(AppContext)
。
这import { Context as AppContext } from "./Context"
有助于我们从文件中导入上下文Context,js
。
该const { articles } = useContext(AppContext)
表达式使用了useContext()
React hooks,它接受AppContext
作为参数,并返回我们从 导入的当前上下文值Context.js
。使用 JavaScript 的解构特性,我们用 数组创建一个常量,可以访问articles
该常量。AppContext
这样,我们ContextProviderSearch
现在就可以访问Context了Context.js
。
简而言之,您可以使用useContext
React hook 访问您在应用程序中创建的任何Context ,以便访问给定Context管理的状态。
SearchContext.js文件包含一些超出本文讨论范围的逻辑。如果您对此有任何疑问,请随时问我。
该项目需要改进的地方
我创建这个项目的目的是为了教育。有几个地方可以改进。我将在下面列出其中一些,以防您好奇或在查看代码库时已经发现它们:
- 测试:应该添加额外的单元测试来检查上下文数据管理是否良好。此外,为后端 NodeJS API 添加测试也是一个好主意。
- 数据存储:出于教育目的,将文章存储在文件系统中是可以的。不过,将 SQL 或 NoSQL 数据库集成到项目中会更好。一些选项包括 Posgres 和 Squelize 作为 ORM,或者 MongoDB 和 Mongoose 作为 DRM。
- 浏览器数据存储:通过 NodeJS API获取数据后,
articles
数据会临时存储在存储对象中。存储对象有大小限制,处理多篇文章时可能不够用。Window.localStorage
Context.js
Window.localStorage
- 延迟加载:您可以添加延迟加载实用程序来改善 webpack 创建的文件的大小。
- 添加 API 身份验证
- 实现错误边界
- 为 React 应用程序实现类型检查
如果您不熟悉上面列出的概念,请先了解一下,并尝试通过克隆代码库来实现它们。这项练习将增强您的 React 技能。
存储库
您可以在此处找到开源项目。
我希望本文和项目可以作为参考,帮助您了解如何在 React 应用程序中使用Context和Hooks 。
本文最初发布于www.nayibabdala.com
链接:https://dev.to/anayib/react-context-and-hooks-an-open-source-project-to-understand-how-they-work-2h76