让我们通过构建食谱搜索应用程序来学习 React Hooks 和 Context API 我们将要构建的最终版本

2025-05-27

让我们通过构建食谱搜索应用程序来学习 React Hooks 和 Context API

我们将要构建的最终版本

React Hooks这是一篇关于理解和的初学者教程Context API。实际上,这更像是试图向我自己和感兴趣的读者解释这些概念。本文将分为两部分,第一部分重点介绍的基础知识hooks。另一部分将重点介绍更高级的用例和Context API。我们将实现搜索功能,并将其转换为使用来Context API管理状态并避免props drilling

更新:第二部分已上线

我们要怎样学习?

我们将对比一下用类组件和 来处理相同状态逻辑的差异React Hooks。我们将构建一个食谱搜索应用程序来巩固概念,之后我们会喝一瓶红酒来庆祝😃。我相信摆弄代码是最好的学习方式。

我们将要构建的最终版本

我们将构建一个简单的 food2fork 网站克隆版本。这不会是完整版,但我们会使用他们的 API 来获取排名前 30 的食谱。添加一些我们自己的 UI 逻辑来调整状态,看看如何处理它们hooks

最终.gif

先决条件

  • React 基础知识
  • JavaScript ES6 基础知识 [数组方法、解构等]

那么什么是 React Hook?

首先,什么是 React Hook?来自文档

什么是 Hook? Hook 是一种特殊的函数,可以让你“钩住” React 的功能。例如,useState 就是一个 Hook,可以让你向函数组件添加 React 状态。

告诉.jpg

简单来说,钩子允许我们“钩住”特定的 React 功能。例如useState,顾名思义,钩子可以帮助我们在 React 中使用状态功能,而在其他情况下,例如在函数组件中,状态功能是无法使用的。我们将通过构建食谱搜索应用程序来详细解释其语法用法等。

设置

我们将使用 create-react-app 来启动应用程序。我已经创建了一个包含 create-react 应用基本框架的 repo,方便我们快速上手。只需克隆即可继续操作。运行以下命令:

git clone https://github.com/olajohn-ajiboye/Blog-React-Hook-Tutorial.git
cd Blog-React-Hook-Tutorial

回到我们将要构建的应用。我们将使用 food2fork API 来获取并搜索 30 个热门食谱列表。但是,该 API 每天的查询次数有限。为了方便本教程,我创建了精确的 JSON 响应。该响应将从此处提供,这样我们就不会过于频繁地访问他们的服务器。

让我们在src文件夹中创建一个组件文件夹,用于存放应用可能用到的不同组件。想象一下,我们会有一个组件显示每个菜谱Recipe,一个RecipeList组件渲染菜谱列表,以及一个RecipeSearch组件和RecipeDetails组件显示每个菜谱的附加详细信息Recipe。所有组件都将是函数式组件,因为使用钩子是本教程的重点。

如果你所有操作都正确,你应该会得到如下所示的文件夹结构。你也可以从此处second克隆仓库中的分支,跳转到本教程的此处。

文件夹结构.PNG

如果你还没有安装,我推荐你安装一个扩展,那就是。它允许你输入简写来获取一些 React 代码片段,从而加快你的 React 开发速度。你可以在这里ES7 React/Redux/GraphQL/React-Native snippets了解更多信息。

让我们Hooked

为什么还要 Hooks?

ezgif.com-optimize.gif

在任何前端应用程序中,最常做的事情之一就是获取和显示数据,并操作显示以获得出色的用户体验。React 也不例外。状态的常见用例之一是存储来自 API 调用的数据。之前hooks,如果您需要在应用程序中使用state任何类型的组件,则必须使用类组件。您还需要在生命周期内异步获取数据。对于许多人来说,这不是一个大问题,但 React 团队认为这会导致组件逻辑的紧密耦合。此外,在更复杂的应用程序中,很难重用状态逻辑。不要轻信我的话,只需阅读此处componentDidMountHooks 的动机即可。

让我们先看看如何从我在经典组件中创建的 REST API 中获取数据,然后再讨论如何使用hooks


import React, { Component } from 'react'
import RecipeList from './components/RecipeList

export default class test extends Component {
  constructor(props) {
    super(props)
    this.state = {
      apiResponse: [],
    }
  }
  componentDidMount() {
    fetch(`https://api.myjson.com/bins/t7szj`)
      .then(data => data.json)
      .then(apiResponse => this.setState({ apiResponse }))
  }
  render() {
    return (
      <div>
          <RecipeList recipes={this.state.recipes}>
      </div>
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

让我们看一下带有 Hook 和 Effect 的相同代码,然后进行解释


import React, { useState, useEffect } from 'react';
import RecipeList from './components/RecipeList

function App() {
  const url = useState(`https://api.myjson.com/bins/t7szj`)
  const [recipes, setRecipes] = useState([])

  const fetchRecipe = async () => {
    const recipeData = await fetch(url)
    const { recipes } = await recipeData.json()
    setRecipes(recipes)
  }

  useEffect(() => {
    fetchRecipe()
  })

  return (
    <div className="App">
      <RecipeList recipes={recipes}>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

有几件事显而易见,我们从 导入了 useState 和 useEffect react。这些是暴露给我们的 API,使我们能够使用React Hooks。HookuseState接受初始状态。在上面的示例中,我们将其初始化为一个空数组。我们希望用 API 调用中的数据填充该数组。这相当于我们类组件中的以下代码。

 this.state = {
      apiResponse: [],
    }
Enter fullscreen mode Exit fullscreen mode

此外,它useState还会返回一对值给我们。它们是当前状态和一个用于更新状态的函数。这样我们就可以从使用状态中返回。这就是我们在应用程序中[currentState, setStateFunction]编写的原因。其中,是一个用于保存配方数据的数组。是使我们能够更新状态的函数,这相当于在类组件中。const [recipes, setRecipes] = useState([])recipessetRecipethis.setState

语法可能看起来令人困惑,这些并非React特有的语法,而是普通的 ES6 JavaScript。这被称为解构。由于useState返回一对值,我们将其解构为一个数组。我们为它们选择的名称不会影响它们的行为,将它们命名为 只是一种良好的习惯[name of your state, set+name of state],因此我们有:

const [recipes, setRecipes] = useState([])

Enter fullscreen mode Exit fullscreen mode

如果您需要一些关于解构的复习或入门知识,我在这里写了一些相关内容

为了充分理解这里发生的事情,我们需要注意的另一个 JavaScript 特性是closures。由于,我们可以在函数内部的任何地方Javascript closure访问解构变量。因此,在函数内部以及 内的任何地方,我们都可以使用或任何其他变量,而无需调用它等等。useStatefecthRecipecomponentsetRecipethis.setRecipe

就本教程而言,作用域的简单定义closures是,它使我们能够访问外部(封闭)函数的变量(作用域链)及其返回值。

您可以在这里这里阅读更多内容并在这里观看

让我们快速将 props 传递给组件,并设置它们以显示 Recipe 列表。由于这不是一个Hook特定的功能,我将跳过它。您可以在这里找到到目前为止的更新仓库。我还添加了样式以加快速度。更新后的仓库位于仓库third/hook-in-app.js的分支上

使用useState

此时,您的App.js代码应该如下所示,我们只是将状态中的食谱数组作为 传递recipes propsRecipeList组件。注意,我还添加了一个加载状态,useState并在数据完全获取后将其设置回false。这是使用多个状态的第一个示例。


import React, { useState, useEffect } from 'react';
import RecipeList from './components/RecipeList'
import RecipeDetails from './components/RecipeDetails'


function App() {
  const url = `https://api.myjson.com/bins/t7szj`
  const [recipes, setRecipes] = useState([])
  const [loading, setLoading] = useState(true)
  const fetchRecipe = async () => {
    const recipeData = await fetch(url)
    const { recipes } = await recipeData.json()
    setRecipes(recipes)
    setLoading(false)
  }
  useEffect(() => {
    fetchRecipe()
  })
  return (
    <div>
      {loading ? <h1 className="text-center">...loading</h1> : <RecipeList recipes={recipes} />}
      <RecipeDetails></RecipeDetails>
    </div>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

接下来,让我们转到RecipeList组件并看看我们有什么。

这里我们只是从父组件接收了recipes作为 传递过来的,并立即执行了它——参见第 5行。然后我们对其进行了映射,将每个配方作为传递给组件。这里没什么特别有趣的。propAppdestructuredRecipeprop

import React from 'react'
import Recipe from './Recipe'
import RecipeSearch from './RecipeSearch'

export default function RecipeList({ recipes }) {

  return (
    <>
      <RecipeSearch></RecipeSearch>
      <div className="container my-5">
        <div className="row">
          <div className="col-10-mx-auto  col-md-6 text-center text-uppercase mb-3">
            <h1 className="text-slaned text-center">Recipe List</h1>
          </div>
        </div>
        <div className="row">
          {recipes.map(recipe => {
            return <Recipe key={recipe.recipe_id} recipe={recipe} />
          })}
        </div>
      </div>
    </>
  )
}

Enter fullscreen mode Exit fullscreen mode

现在到了有趣的部分。

在组件内部Recipe,我添加了一些states足够简单易懂的实现。我们将尝试逐行逐行地讲解我们正在做的事情,以及我们如何用 处理状态逻辑useState hook。你的组件中应该包含以下内容Recipe

import React, { useState } from 'react'

export default function Recipe({ recipe }) {
  const { image_url, publisher, title, recipe_id } = recipe
  const [showInfo, setShowInfo] = useState(false)
  const [recipeDetails, setRecipeDetails] = useState([])
  const { ingredients, social_rank } = recipeDetails

  const handleShowInfo = async (e) => {
    const { id } = e.target.dataset
    const response = await fetch(`https://www.food2fork.com/api/get?key=7cdab426afc366070dab735500555521&rId=${id}`)
    const { recipe } = await response.json()
    setRecipeDetails(recipe)
    setShowInfo(!showInfo)
  }
  return (
    <>
      <div className="col-10 mx-auto col-md-6 col-lg-4 my-3">
        <div className="card">
          <img src={image_url} alt="recipe" className="img-card-top" style={{ height: "14rem" }} />
          <div className="card-body text-capitalize">
            <h6>{title}</h6>
            <h6 className="text-warning">
              Provided by: {publisher}
            </h6>
          </div>
          <div className="card-footer">
            <button type="button" style={{ margin: `13px` }} className="btn btn-primary text-center" data-id={recipe_id} onClick={handleShowInfo}>More Info</button>
            {showInfo &&
              <button key={recipe_id} type="button" style={{ margin: `13px` }} className="btn btn-success text-center font-weight-bold" >{social_rank}</button>}
            {showInfo ?
              ingredients.map((i, index) => {
                return <ul key={index} className="list-group">
                  <li className="list-group-item" >{i}</li>
                </ul>
              })
              : null}
          </div>
        </div>
      </div>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

让我们理解一下上面的代码。和之前一样,我们从它的父组件(也就是)中接收了 arecipe作为 a ,然后立即在函数参数中对其进行了解构。接下来,我们进一步解构了对象中我们想要使用的部分。我知道对象包含什么,因为我已经测试过 API,所以这并不算什么魔法。这相当于下面的代码:propRecipeListreciperecipe

export default function Recipe(props) {
    const recipe = this.props.recipe
    const { image_url, publisher, title, recipe_id } = recipe

}
Enter fullscreen mode Exit fullscreen mode

现在来看看一些有用的状态。如果你检查过这个应用程序,你会发现,当我们点击“更多详情”按钮时,我们会获得与该菜谱相关的额外信息,特别是list of ingredientsocial rating。看一下上面的动图来复习一下。所以我们需要某种状态来处理所需的用户交互。

想一想,我们需要一种方法来切换是否显示更多信息。我们还需要一种方法来获取特定菜谱的信息。然后,所需的结果将存储在某种状态中。瞧,我们已经确定了至少两种状态。因此,在我们的应用程序中,我们有showInforecipeDetails状态。

利用我们掌握的信息,我们可以用它来useState Hook解决这个问题。

  • 首先,我们声明showInfo状态和设置 showInfo 的函数setShowInfo(相当于this.SetState)。我们将值设置为false
  • 其次,我们声明recipeDetailssetRecipeDetails。我们将值设置为空数组[]

希望这足够简单,我们已经设置了初始状态。并准备好使用 和 来处理状态setShowInfo变化setRecipeDetails

转到handleShowInfo函数。这是一个async基本上用于获取数据的函数。它还处理状态变化以显示或不显示信息。让我们逐行分解它。
由于我们打算handleShowInfo在点击按钮时调用,所以我们可以访问事件对象。在按钮内部,我们将 设置recipe_iddata-attribute。这使我们能够获取id特定菜谱的 。然后,在 内部,我们通过从 属性中提取 来handleShowInfo,获取。由于我们需要获取更多信息,因此需要使用 发出请求。这是我们接下来要做的事情,然后等待响应。然后,我们将值转换为并将值存储在 中idevent.targetHTTPidjsonconst recipe

*注意:*您可能需要从food2fork获取 API 密钥。当前密钥可能超出限制。

我们得到的响应recipe是插入到 内部的,setRecipeDetails用作 的更新器recipeDetails。此时,我们只是将 的状态设置recipeDetails为数组响应变量recipe。这相当于

this.setState{
recipedDetails: recipe
}
Enter fullscreen mode Exit fullscreen mode

另外,我们将 的值设置showInfo为与之前相反的值。这就是每次点击按钮时都会产生切换效果的原因。这相当于。

this.setState{
showInfo: !showInfo
}
Enter fullscreen mode Exit fullscreen mode

就是这样,在返回结果中,jsx我们根据按钮点击的状态有条件地渲染了信息showInfo。我们还额外映射了成分数组,以将其作为附加信息显示。

这篇非常基础的介绍到此结束hooks,或许有些过于简化。在本系列的下一篇中,我们将更详细地探讨钩子,然后学习Context API……

希望你喜欢这篇介绍。欢迎提供反馈。敬请期待下次更新,期待很快与你见面。谢谢!

谢谢你-meme-03.jpg

文章来源:https://dev.to/mongopark/let-s-learn-react-hooks-and-context-api-by-building-a-recipe-search-app-39pc
PREV
金丝雀部署 部署策略简介:蓝绿部署、金丝雀部署等
NEXT
DEV,满足站点可靠性工程