Next.js API Application :: TypeError: Cannot read property 'map' of undefined

1k Views Asked by At

I have just been introduced to Next.js and have been tasked to create a dynamic website that uses data retrieved from an API. The web app should contain at least two pages: an index page and a page that displays details about the topic that the user selects on the index page.

I have chosen to make use of the Edamam recipe API and to use the search functionality on the index/ home page to render the recipe results on the page to fulfil the brief. I am, however, experiencing some trouble iterating over the data.

Please see below the error:

enter image description here

My code is as follows:

- Pages:

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;

recipes.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;

- Components:

Header.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;

RecipeData.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;

I have run console.logs on recipes (SearchForm.js - empty array returned), props (RecipeData.js - empty object returned) and on res.data.hits (SearchForm.js - returned data).

I seem to be having trouble defining the props in the pages/ components, but am not having any success sorting it out.

I would appreciate it if anyone is willing to assist.

1

There are 1 best solutions below

7
Naim Mustafa On

based on the pictures at the beginning api is returning 500 internal server error, in this function adda error handler sendApiRequest if the error occurs you can capture and display a message or anything you want tbh.

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})
  };

when it comes to the map error, you can either define defaultProps in case the props of your components are undefined or null or you can add ?

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

it will not throw an error if you add a null check like this

EDIT: Based on your comment below, your issue is related to the state management. I highly recomend you to use redux or any other state management if you can

but ill explain how should the state management should be with the useState hook


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