首页 > 解决方案 > Next.js API 应用程序 :: TypeError: 无法读取未定义的属性“地图”

问题描述

我刚刚被介绍给 Next.js,我的任务是创建一个使用从 API 检索到的数据的动态网站。Web 应用程序应至少包含两个页面:一个索引页面和一个显示用户在索引页面上选择的主题的详细信息的页面。

我选择使用 Edamam 食谱 API 并使用索引/主页上的搜索功能在页面上呈现食谱结果以完成简介。但是,我在迭代数据时遇到了一些麻烦。

请看下面的错误:

在此处输入图像描述

我的代码如下:

- 页面:

index.js

// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// import { Spinner } from "@chakra-ui/react";
// Imported AppDisplay to set the holistic style of this page.
import AppDisplay from "../components/AppDisplay";
// Imported Carousel from React Bootstrap.
import { Carousel } from "react-bootstrap";
// Importing the SearchForm component.
import SearchForm from "../components/SearchForm";

/**
 * Styled the home page.
 */

const carouselStyle = {
  overflowX: "hidden",
  overflowY: "hidden",
  height: "auto",
  width: "auto",
};

const logoStyle = {
  height: "450px",
  width: "auto",
  marginBottom: "70px",
};

/**
 * Applied the styles and passed the AppDisplay props.
 * @returns Styled home page, displaying a styled introduction header text section an image and a header component.
 */

const Home = (props) => {
  const { search, onInputChange } = props;

  return (
    <div>
      <AppDisplay>
        <div>
          <Carousel variant="dark" style={carouselStyle}>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Breakfast.jpg"
                alt="First slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Dinner.jpg"
                alt="Second slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Dessert.jpg"
                alt="Third slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Bake.jpg"
                alt="Third slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Burger.jpg"
                alt="Third slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Casserole.jpg"
                alt="Third slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Pizza.jpg"
                alt="Third slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
              </Carousel.Caption>
            </Carousel.Item>
            <Carousel.Item>
              <img
                className="d-block w-100"
                src="/static/images/Pudding.jpg"
                alt="Third slide"
              />
              <Carousel.Caption>
                <img
                  src="/static/images/GrumbleLogoMain.png"
                  alt="Grumble Logo"
                  style={logoStyle}
                />
                <SearchForm value={search} onChange={onInputChange} />
                <div id="edamam-badge" data-color="white" z-index="1"></div>
              </Carousel.Caption>
            </Carousel.Item>
          </Carousel>
          {/* <div id="edamam-badge" data-color="white"></div> */}
        </div>
      </AppDisplay>
    </div>
  );
};

// Exported home page to be generated.
export default Home;

食谱.js

// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Imported AppDisplay to set the holistic style of this page.
import AppDisplay from "../components/AppDisplay";
// Imported Recipe component.
import RecipeData from "../components/RecipeData";
import Header from "../components/Header";

const Recipes = (props) => {
  const { recipes } = props;
  console.log("props:", props);

  const recipeDetails = recipes.map(({ recipe }) => ({
    label: recipe.recipe.label,
    source: recipe.recipe.source,
    totalTime: recipe.recipe.totalTime,
    cuisineType: recipe.recipe.cuisineType,
    mealType: recipe.recipe.mealType,
    healthLabels: recipe.recipe.healthLabels,
    dietLabels: recipe.recipe.dietLabels,
    image: recipe.recipe.image,
    ingredientLines: recipe.recipe.ingredientLines,
    url: recipe.recipe.url,
  }));

  return (
    <div>
      <AppDisplay />
      <Header />
      <div>
        {recipeDetails.map((recipes) => (
          <RecipeData recipes={recipes} />
        ))}
      </div>
    </div>
  );
};

// Exported home page to be generated.
export default Recipes;

- 成分:

页眉.js

// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Imported Font Awesome library and icons. Also added "import "@fortawesome/fontawesome-svg-core/styles.css";" to allow styling the icons.
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import "@fortawesome/fontawesome-svg-core/styles.css";

/**
 * Styled the header component.
 */

// Setting the header's position to absolute and set the padding and background color to transparent.
const headerStyle = {
  // position: "absolute",
  height: "auto",
  width: "auto",
  display: "flex",
  flexDirection: "row",
  padding: 5,
  backgroundColor: "#393d49",
  zIndex: 1,
};

// Set the size (height x width) of the header's logo.
const logoStyle = {
  height: "80px",
  width: "auto",
};

// Set the margins and the font color, size and decoration of the header links.
const linkStyle = {
  margin: "auto 40px auto 20px",
  color: "#ffffff",
  fontSize: 20,
  textDecoration: "none",
};

// Set the recipe page's visibility to hidden.
const recipeLinkStyle = {
  visibility: "hidden",
};

// Created onMouseOver and onMouseOut event handler functions to change the font colors of the header links once hovered
// over and to change back the colour when the links are no longer hovered over.
const changeFontColor = (e) => {
  e.target.style.color = "#f1b374";
};

const changeBackFontColor = (e) => {
  e.target.style.color = "#ffffff";
};

// Set the font size and the right margin of the home icon.
const iconStyle = {
  fontSize: "1.1rem",
  marginRight: "5px",
  color: "#ffffff",
};

/**
 * Attached the event handlers to the links with onMouseOver and onMouseOut.
 * @returns The styled header component with navigatable, styled links.
 */

const Header = () => (
  <div style={headerStyle}>
    <img
      src="/static/images/GrumbleLogoHead.png"
      alt="Grumble Logo"
      style={logoStyle}
    />
    <Link href="/">
      <a
        style={linkStyle}
        onMouseOver={changeFontColor}
        onMouseOut={changeBackFontColor}
      >
        <FontAwesomeIcon icon={faHome} style={iconStyle} />
        Home
      </a>
    </Link>
    <Link href="/recipes">
      <a style={recipeLinkStyle}>RECIPES</a>
    </Link>
  </div>
);

// Exporting the Header to the recipe page.
export default Header;

AppDisplay.js

// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Importing the Next built-in component to append elements to the head of the page.
import Head from "next/head";

/**
 * Created a global style.
 */

// Set the application's margins, padding and font size and families. Also set for the vertical and horizontal overflow to be hidden.
const appDisplayStyle = {
  margin: 0,
  padding: 0,
  overflowX: "hidden",
  overflowY: "hidden",
  fontSize: 15,
  fontFamily: "Staatliches, Trebuchet, Helvetica",
};

/**
 * Added the links to utilize React Bootstrap and the Google font.
 * @param {*} props Children pages for appDisplayStyle to render - index, recipes.
 * @returns The application's general styling, with appended links, for use in the pages.
 */

const AppDisplay = (props) => (
  <div>
    <Head>
      <link
        rel="stylesheet"
        href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
        integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
        crossOrigin="anonymous"
      />
      <link
        rel="stylesheet"
        href="https://fonts.googleapis.com/css?family=Staatliches"
      />
      {/* <script src="https://developer.edamam.com/attribution/badge.js"></script> */}
    </Head>
    <div style={appDisplayStyle}>{props.children}</div>
  </div>
);

// Exporting AppDisplay for use on the pages.
export default AppDisplay;

SearchForm.js

// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Imported React library and hooks.
import { useEffect, useState } from "react";
// Requiring Axios.
import axios from "axios";
// Imported components from React Bootstrap.
import { Form, FormControl } from "react-bootstrap";
// Imported Font Awesome library and icons. Also added "import "@fortawesome/fontawesome-svg-core/styles.css";" to allow styling the icons.
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import "@fortawesome/fontawesome-svg-core/styles.css";

/**
 * Styled the SearchForm component.
 */

// Set the search container's position to absolute and aligned it to the top and left. Also set the left margin to counter the left position.
const searchContainer = {
  position: "absolute",
  top: "68%",
  left: "45.5%",
  marginLeft: "-100px",
};

// Set for the form container to display as flex and the direction to row. Also set the position to relative to allow the icon to appear inside
// the input area.
const formContainer = {
  display: "flex",
  flexDirection: "row",
  position: "relative",
};

// Set the size (height x width), the padding and the background color of the input element.
const searchInputStyle = {
  height: "35px",
  width: 300,
  padding: 5,
  backgroundColor: "#ffffff",
};

// Set the icon's position to absolute and aligned it to the top and left. Also set the height, the font size and color and for the cursor to
// to a pointer once it hovers over the icon.
const iconStyle = {
  position: "absolute",
  left: "275px",
  top: "8px",
  height: "20px",
  fontSize: "1rem",
  color: "#808080",
  cursor: "pointer",
};

const SearchForm = () => {
  const [recipes, setRecipes] = useState([]);
  const [search, setSearch] = useState("");
  console.log("recipes:", recipes);

  const API_ID = "some_sensitive_data";
  const API_KEY = "some_sensitive_data";

  useEffect(() => {
    sendApiRequest();
    return () => {
      setRecipes({});
    };
  }, []);

  // An asynchronous function fetching data from the API.
  const sendApiRequest = async () => {
    const res = await axios.get(
      //   `https://api.edamam.com/search?q=${search}&app_id=${API_ID}&app_key=${API_KEY}`
      `https://api.edamam.com/search?q=bacon&app_id=${API_ID}&app_key=${API_KEY}&from=0&to=12`
    );
    // const data = await res.json();
    setRecipes(res.data.hits);
    console.log("res.data.hits:", res.data.hits);
  };

  const onInputChange = (e) => {
    setSearch();
    console.log(e.target.value);
  };

  return (
    <div>
      <div style={searchContainer}>
        <Form
          className="search-form"
          style={formContainer}
          onSubmit={sendApiRequest}
        >
          <FormControl
            type="text"
            placeholder="Search"
            className="search-bar mr-sm-2"
            style={searchInputStyle}
            onChange={onInputChange}
            value={search}
            //   isDisabled={isLoading}
          />
          <a href="/recipes">
            <FontAwesomeIcon
              icon={faSearch}
              style={iconStyle}
              type="submit"
              className="search-button"
              id="search"
              onClick={sendApiRequest}
            />
          </a>
        </Form>
      </div>
    </div>
  );
};

// Exported the RecipeListings to SearchForm.
export default SearchForm;

食谱数据.js

// Imported React library and hooks.
// import { useEffect, useState } from "react";
import { Card, Button } from "react-bootstrap";

const RecipeData = (props) => {
  console.log('props:', props)
  const {
    label,
    source,
    totalTime,
    cuisineType,
    mealType,
    healthLabels,
    dietLabels,
    image,
    ingredientLines,
    url,
  } = props;

  return (
    <Card col-3 offset-1>
      <Card.Header>
        <h5>{label}</h5>
        <table>
          <tr>
            <th>Recipe By:</th>
            <td>{source}</td>
          </tr>
          <tr>
            <th>Preparation Time:</th>
            <td>{totalTime}</td>
          </tr>
          <tr>
            <th>Cuisine:</th>
            <td>{cuisineType}</td>
          </tr>
          <tr>
            <th>Meal Type:</th>
            <td>{mealType}</td>
          </tr>
          <tr>
            <th>Health:</th>
            <td>{healthLabels}</td>
          </tr>
          <tr>
            <th>Dietary Information:</th>
            <td>{dietLabels}</td>
          </tr>
        </table>
      </Card.Header>
      <Card.Img src={image} alt="Recipe Photograph" />
      <Card.Body>
        <ul>
          {ingredientLines.map((ingredients) => (
            <li>{ingredients}</li>
          ))}
        </ul>
      </Card.Body>
      <Card.Footer>
        <Button href={url} target="_blank">
          Method and More
        </Button>
      </Card.Footer>
    </Card>
  );
};

// Exported recipeDetails to be generated.
export default RecipeData;

我已经在食谱(SearchForm.js - 返回空数组)、道具(RecipeData.js - 返回空对象)和 res.data.hits(SearchForm.js - 返回数据)上运行 console.logs。

我似乎在定义页面/组件中的道具时遇到了麻烦,但没有成功对其进行分类。

如果有人愿意提供帮助,我将不胜感激。

标签: reactjsapinext.jsedamam-api

解决方案


根据开头的图片 api 返回 500 内部服务器错误,在此函数中添加错误处理程序sendApiRequest,如果发生错误,您可以捕获并显示消息或任何您想要的内容。

const sendApiRequest = async () => {
    axios.get(`https://api.edamam.com/search?q=bacon&app_id=${API_ID}&app_key=${API_KEY}&from=0&to=12`
    ).then(res => {
 setRecipes(res.data.hits);
}).catch(err => console.log('there was an error in sendApiRequest ', {err})
  };

当涉及到地图错误时,您可以定义 defaultProps 以防组件的道具未定义或为空,或者您可以添加?

<div>
      <AppDisplay />
      <Header />
      <div>
        {recipeDetails?.map((recipes) => (
          <RecipeData recipes={recipes} />
        ))}
      </div>
    </div>

如果您添加这样的空检查,它不会抛出错误

编辑:根据您在下面的评论,您的问题与状态管理有关。如果可以的话,我强烈建议您使用 redux 或任何其他状态管理

但我无法解释状态管理应该如何与 useState 挂钩


import React, {useState} from 'react'



const MainComponent = () => {
const [someState, setSomeState] = useState([])


return (
<>
// this is where you are sending the request to the api
   <ComponentFetchesTheData fetchData={setSomeState} />
// this is where you display the results
   <ComponentDisplaysTheData data={someState} />
</>

)
}

export default MainComponent

推荐阅读