Building a Recipe Search App with React and Edamam API

This tutorial will guide you through the process of building a recipe search app using React and the Edamam API. We will start by setting up the project and installing necessary dependencies. Then, we will build the search component, handle user input, and make API requests. Next, we will display the search results, implement pagination, and add sorting and filtering options. Finally, we will fetch recipe details, display recipe information, and implement favorite functionality. Along the way, we will also cover styling the app using CSS frameworks, customizing styles, and considering responsive design.

building recipe search app react edamam api

Introduction

React is a popular JavaScript library for building user interfaces. It allows developers to create reusable UI components and efficiently update the DOM. The Edamam API is a powerful tool for accessing a vast collection of recipe data. By combining React with the Edamam API, we can create a recipe search app that provides users with an easy way to find and explore various recipes.

Setting Up the Project

Creating a new React project

To start building our recipe search app, we need to create a new React project. We can do this by using the create-react-app command-line tool. Open your terminal and run the following command:

npx create-react-app recipe-search-app

This will create a new directory called recipe-search-app with all the necessary files and dependencies for a React project.

Installing necessary dependencies

Next, we need to install the necessary dependencies for our project. Open your terminal, navigate to the recipe-search-app directory, and run the following command:

npm install axios react-router-dom

This will install the axios library, which we will use for making API requests, and the react-router-dom library, which we will use for routing within our app.

Setting up API credentials

Before we can start making API requests to the Edamam API, we need to obtain API credentials. Visit the Edamam Developer portal and sign up for a free account. Once you have an account, create a new application and you will be provided with an API key and an API ID. Take note of these credentials as we will need them later.

Building the Search Component

Creating the search form

In order to allow users to search for recipes, we need to create a search form component. Create a new file called SearchForm.js in the src directory and add the following code:

import React, { useState } from 'react';

const SearchForm = () => {
  const [query, setQuery] = useState('');

  const handleInputChange = (event) => {
    setQuery(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    // Make API request using the query value
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={query} onChange={handleInputChange} />
      <button type="submit">Search</button>
    </form>
  );
};

export default SearchForm;

In this code, we use the useState hook to create a state variable called query and a corresponding setter function called setQuery. The handleInputChange function updates the query state variable whenever the user types into the input field. The handleSubmit function is called when the form is submitted, preventing the default form submission behavior. Finally, we render a form element with an input field and a submit button, with the handleSubmit function as the form's onSubmit event handler.

Handling user input

Now that we have the search form component, we need to handle the user's search input and make API requests based on that input. To do this, we will create a new file called SearchPage.js in the src directory and add the following code:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);

  const handleSearch = async (query) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}`
      );
      setResults(response.data.hits);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      {/* Render search results */}
    </div>
  );
};

export default SearchPage;

In this code, we import the SearchForm component that we created earlier. We also import the axios library for making API requests. We use the useState hook to create a state variable called results and a corresponding setter function called setResults. The handleSearch function is called when the form is submitted, and it makes an API request to the Edamam API using the query value. The API response is then stored in the results state variable. Finally, we render the SearchForm component and display the search results.

Displaying Search Results

Rendering search results

Now that we have the search results stored in the results state variable, we can render them in our app. Modify the SearchPage component in the SearchPage.js file as follows:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);

  const handleSearch = async (query) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}`
      );
      setResults(response.data.hits);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      <ul>
        {results.map((result) => (
          <li key={result.recipe.uri}>
            <h2>{result.recipe.label}</h2>
            <img src={result.recipe.image} alt={result.recipe.label} />
            <p>Source: {result.recipe.source}</p>
            <p>Calories: {result.recipe.calories}</p>
            {/* Add more recipe information */}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default SearchPage;

In this code, we render an unordered list element (<ul>) to display the search results. We use the map function to iterate over the results array and render a list item (<li>) for each result. Inside each list item, we display the recipe label, image, source, and calories.

Handling pagination

The Edamam API provides pagination functionality, allowing us to retrieve a specific set of search results. To implement pagination in our app, we need to modify the SearchPage component in the SearchPage.js file as follows:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);
  const [currentPage, setCurrentPage] = useState(0);

  const handleSearch = async (query, page = 0) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}&from=${page * 10}`
      );
      setResults(response.data.hits);
      setCurrentPage(page);
    } catch (error) {
      console.error(error);
    }
  };

  const handlePagination = (direction) => {
    const nextPage = direction === 'next' ? currentPage + 1 : currentPage - 1;
    handleSearch(query, nextPage);
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      <ul>
        {results.map((result) => (
          <li key={result.recipe.uri}>
            <h2>{result.recipe.label}</h2>
            <img src={result.recipe.image} alt={result.recipe.label} />
            <p>Source: {result.recipe.source}</p>
            <p>Calories: {result.recipe.calories}</p>
          </li>
        ))}
      </ul>
      <button onClick={() => handlePagination('prev')} disabled={currentPage === 0}>
        Previous
      </button>
      <button onClick={() => handlePagination('next')}>Next</button>
    </div>
  );
};

export default SearchPage;

In this code, we add a new state variable called currentPage to keep track of the current page of search results. The handleSearch function now accepts an optional page parameter, which is used to specify the starting index of the search results. We update the API request URL to include the from parameter, which is calculated based on the current page. We also add a new handlePagination function, which is called when the previous or next button is clicked. This function updates the currentPage state variable and calls the handleSearch function with the new page value. Finally, we render the previous and next buttons, disabling the previous button if we are on the first page.

Adding sorting and filtering options

The Edamam API provides sorting and filtering options to refine search results. To implement sorting and filtering in our app, we need to modify the SearchPage component in the SearchPage.js file as follows:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);
  const [currentPage, setCurrentPage] = useState(0);
  const [sortOption, setSortOption] = useState('');
  const [dietOption, setDietOption] = useState('');

  const handleSearch = async (query, page = 0) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}&from=${page * 10}&sort=${sortOption}&diet=${dietOption}`
      );
      setResults(response.data.hits);
      setCurrentPage(page);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSortChange = (event) => {
    setSortOption(event.target.value);
  };

  const handleDietChange = (event) => {
    setDietOption(event.target.value);
  };

  const handlePagination = (direction) => {
    const nextPage = direction === 'next' ? currentPage + 1 : currentPage - 1;
    handleSearch(query, nextPage);
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      <div>
        <label>
          Sort By:
          <select value={sortOption} onChange={handleSortChange}>
            <option value="">None</option>
            <option value="r">Relevance</option>
            <option value="t">Title</option>
            <option value="i">Ingredients</option>
          </select>
        </label>
        <label>
          Diet:
          <select value={dietOption} onChange={handleDietChange}>
            <option value="">None</option>
            <option value="balanced">Balanced</option>
            <option value="high-protein">High-Protein</option>
            <option value="low-carb">Low-Carb</option>
            <option value="low-fat">Low-Fat</option>
          </select>
        </label>
      </div>
      <ul>
        {results.map((result) => (
          <li key={result.recipe.uri}>
            <h2>{result.recipe.label}</h2>
            <img src={result.recipe.image} alt={result.recipe.label} />
            <p>Source: {result.recipe.source}</p>
            <p>Calories: {result.recipe.calories}</p>
          </li>
        ))}
      </ul>
      <button onClick={() => handlePagination('prev')} disabled={currentPage === 0}>
        Previous
      </button>
      <button onClick={() => handlePagination('next')}>Next</button>
    </div>
  );
};

export default SearchPage;

In this code, we add two new state variables called sortOption and dietOption to keep track of the selected sorting and filtering options. We also add two new event handler functions called handleSortChange and handleDietChange, which update the corresponding state variables when the select elements change. We update the API request URL to include the sort and diet parameters, which are based on the selected options. Finally, we render two select elements for sorting and filtering, with options for each available option.

Adding Recipe Details

Fetching recipe details

Now that we have the search results displayed, we can add functionality to fetch and display the details of each recipe. To do this, we need to modify the SearchPage component in the SearchPage.js file as follows:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);
  const [currentPage, setCurrentPage] = useState(0);
  const [sortOption, setSortOption] = useState('');
  const [dietOption, setDietOption] = useState('');
  const [selectedRecipe, setSelectedRecipe] = useState(null);

  const handleSearch = async (query, page = 0) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}&from=${page * 10}&sort=${sortOption}&diet=${dietOption}`
      );
      setResults(response.data.hits);
      setCurrentPage(page);
      setSelectedRecipe(null);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSelectRecipe = async (recipeUri) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?r=${encodeURIComponent(recipeUri)}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}`
      );
      setSelectedRecipe(response.data[0]);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSortChange = (event) => {
    setSortOption(event.target.value);
  };

  const handleDietChange = (event) => {
    setDietOption(event.target.value);
  };

  const handlePagination = (direction) => {
    const nextPage = direction === 'next' ? currentPage + 1 : currentPage - 1;
    handleSearch(query, nextPage);
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      <div>
        <label>
          Sort By:
          <select value={sortOption} onChange={handleSortChange}>
            <option value="">None</option>
            <option value="r">Relevance</option>
            <option value="t">Title</option>
            <option value="i">Ingredients</option>
          </select>
        </label>
        <label>
          Diet:
          <select value={dietOption} onChange={handleDietChange}>
            <option value="">None</option>
            <option value="balanced">Balanced</option>
            <option value="high-protein">High-Protein</option>
            <option value="low-carb">Low-Carb</option>
            <option value="low-fat">Low-Fat</option>
          </select>
        </label>
      </div>
      <ul>
        {results.map((result) => (
          <li key={result.recipe.uri}>
            <h2>{result.recipe.label}</h2>
            <img src={result.recipe.image} alt={result.recipe.label} />
            <p>Source: {result.recipe.source}</p>
            <p>Calories: {result.recipe.calories}</p>
            <button onClick={() => handleSelectRecipe(result.recipe.uri)}>View Details</button>
          </li>
        ))}
      </ul>
      <button onClick={() => handlePagination('prev')} disabled={currentPage === 0}>
        Previous
      </button>
      <button onClick={() => handlePagination('next')}>Next</button>
      {selectedRecipe && (
        <div>
          <h2>{selectedRecipe.label}</h2>
          <img src={selectedRecipe.image} alt={selectedRecipe.label} />
          <p>Source: {selectedRecipe.source}</p>
          <p>Calories: {selectedRecipe.calories}</p>
          {/* Display more recipe details */}
        </div>
      )}
    </div>
  );
};

export default SearchPage;

In this code, we add a new state variable called selectedRecipe to keep track of the currently selected recipe. We also add a new event handler function called handleSelectRecipe, which is called when the "View Details" button is clicked. This function makes an API request to the Edamam API using the recipe URI and stores the response in the selectedRecipe state variable. Finally, we render the details of the selected recipe if selectedRecipe is not null.

Displaying recipe information

Now that we have the recipe details fetched, we can display them in our app. Modify the SearchPage component in the SearchPage.js file as follows:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);
  const [currentPage, setCurrentPage] = useState(0);
  const [sortOption, setSortOption] = useState('');
  const [dietOption, setDietOption] = useState('');
  const [selectedRecipe, setSelectedRecipe] = useState(null);

  const handleSearch = async (query, page = 0) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}&from=${page * 10}&sort=${sortOption}&diet=${dietOption}`
      );
      setResults(response.data.hits);
      setCurrentPage(page);
      setSelectedRecipe(null);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSelectRecipe = async (recipeUri) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?r=${encodeURIComponent(recipeUri)}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}`
      );
      setSelectedRecipe(response.data[0]);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSortChange = (event) => {
    setSortOption(event.target.value);
  };

  const handleDietChange = (event) => {
    setDietOption(event.target.value);
  };

  const handlePagination = (direction) => {
    const nextPage = direction === 'next' ? currentPage + 1 : currentPage - 1;
    handleSearch(query, nextPage);
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      <div>
        <label>
          Sort By:
          <select value={sortOption} onChange={handleSortChange}>
            <option value="">None</option>
            <option value="r">Relevance</option>
            <option value="t">Title</option>
            <option value="i">Ingredients</option>
          </select>
        </label>
        <label>
          Diet:
          <select value={dietOption} onChange={handleDietChange}>
            <option value="">None</option>
            <option value="balanced">Balanced</option>
            <option value="high-protein">High-Protein</option>
            <option value="low-carb">Low-Carb</option>
            <option value="low-fat">Low-Fat</option>
          </select>
        </label>
      </div>
      <ul>
        {results.map((result) => (
          <li key={result.recipe.uri}>
            <h2>{result.recipe.label}</h2>
            <img src={result.recipe.image} alt={result.recipe.label} />
            <p>Source: {result.recipe.source}</p>
            <p>Calories: {result.recipe.calories}</p>
            <button onClick={() => handleSelectRecipe(result.recipe.uri)}>View Details</button>
          </li>
        ))}
      </ul>
      <button onClick={() => handlePagination('prev')} disabled={currentPage === 0}>
        Previous
      </button>
      <button onClick={() => handlePagination('next')}>Next</button>
      {selectedRecipe && (
        <div>
          <h2>{selectedRecipe.label}</h2>
          <img src={selectedRecipe.image} alt={selectedRecipe.label} />
          <p>Source: {selectedRecipe.source}</p>
          <p>Calories: {selectedRecipe.calories}</p>
          <p>Servings: {selectedRecipe.yield}</p>
          <h3>Ingredients:</h3>
          <ul>
            {selectedRecipe.ingredients.map((ingredient, index) => (
              <li key={index}>{ingredient.text}</li>
            ))}
          </ul>
          <h3>Instructions:</h3>
          <ol>
            {selectedRecipe.instructions.map((instruction, index) => (
              <li key={index}>{instruction}</li>
            ))}
          </ol>
        </div>
      )}
    </div>
  );
};

export default SearchPage;

In this code, we add additional information about the selected recipe, such as servings, ingredients, and instructions. We render the servings, ingredients, and instructions using nested ul and ol elements, respectively.

Implementing favorite functionality

To allow users to mark recipes as favorites, we need to modify the SearchPage component in the SearchPage.js file as follows:

import React, { useState } from 'react';
import SearchForm from './SearchForm';
import axios from 'axios';

const SearchPage = () => {
  const [results, setResults] = useState([]);
  const [currentPage, setCurrentPage] = useState(0);
  const [sortOption, setSortOption] = useState('');
  const [dietOption, setDietOption] = useState('');
  const [selectedRecipe, setSelectedRecipe] = useState(null);
  const [favorites, setFavorites] = useState([]);

  const handleSearch = async (query, page = 0) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?q=${query}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}&from=${page * 10}&sort=${sortOption}&diet=${dietOption}`
      );
      setResults(response.data.hits);
      setCurrentPage(page);
      setSelectedRecipe(null);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSelectRecipe = async (recipeUri) => {
    try {
      const response = await axios.get(
        `https://api.edamam.com/search?r=${encodeURIComponent(recipeUri)}&app_id={YOUR_APP_ID}&app_key={YOUR_API_KEY}`
      );
      setSelectedRecipe(response.data[0]);
    } catch (error) {
      console.error(error);
    }
  };

  const handleFavorite = () => {
    if (selectedRecipe) {
      setFavorites([...favorites, selectedRecipe]);
    }
  };

  const handleSortChange = (event) => {
    setSortOption(event.target.value);
  };

  const handleDietChange = (event) => {
    setDietOption(event.target.value);
  };

  const handlePagination = (direction) => {
    const nextPage = direction === 'next' ? currentPage + 1 : currentPage - 1;
    handleSearch(query, nextPage);
  };

  return (
    <div>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />
      <div>
        <label>
          Sort By:
          <select value={sortOption} onChange={handleSortChange}>
            <option value="">None</option>
            <option value="r">Relevance</option>
            <option value="t">Title</option>
            <option value="i">Ingredients</option>
          </select>
        </label>
        <label>
          Diet:
          <select value={dietOption} onChange={handleDietChange}>
            <option value="">None</option>
            <option value="balanced">Balanced</option>
            <option value="high-protein">High-Protein</option>
            <option value="low-carb">Low-Carb</option>
            <option value="low-fat">Low-Fat</option>
          </select>
        </label>
      </div>
      <ul>
        {results.map((result) => (
          <li key={result.recipe.uri}>
            <h2>{result.recipe.label}</h2>
            <img src={result.recipe.image} alt={result.recipe.label} />
            <p>Source: {result.recipe.source}</p>
            <p>Calories: {result.recipe.calories}</p>
            <button onClick={() => handleSelectRecipe(result.recipe.uri)}>View Details</button>
          </li>
        ))}
      </ul>
      <button onClick={() => handlePagination('prev')} disabled={currentPage === 0}>
        Previous
      </button>
      <button onClick={() => handlePagination('next')}>Next</button>
      {selectedRecipe && (
        <div>
          <h2>{selectedRecipe.label}</h2>
          <img src={selectedRecipe.image} alt={selectedRecipe.label} />
          <p>Source: {selectedRecipe.source}</p>
          <p>Calories: {selectedRecipe.calories}</p>
          <p>Servings: {selectedRecipe.yield}</p>
          <h3>Ingredients:</h3>
          <ul>
            {selectedRecipe.ingredients.map((ingredient, index) => (
              <li key={index}>{ingredient.text}</li>
            ))}
          </ul>
          <h3>Instructions:</h3>
          <ol>
            {selectedRecipe.instructions.map((instruction, index) => (
              <li key={index}>{instruction}</li>
            ))}
          </ol>
          <button onClick={handleFavorite}>Add to Favorites</button>
        </div>
      )}
      {favorites.length > 0 && (
        <div>
          <h2>Favorites</h2>
          <ul>
            {favorites.map((favorite, index) => (
              <li key={index}>{favorite.label}</li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

export default SearchPage;

In this code, we add a new state variable called favorites to keep track of the user's favorite recipes. We also add a new event handler function called handleFavorite, which adds the selected recipe to the favorites array. Finally, we render the favorites at the bottom of the page if there are any.

Styling the App

Using CSS frameworks

To style our recipe search app, we can use CSS frameworks such as Bootstrap or Material-UI. These frameworks provide pre-designed components and utility classes that we can use to quickly style our app. To use a CSS framework, we need to install it and import its styles into our project. For example, to use Bootstrap, we need to run the following command in our terminal:

npm install bootstrap

Once the installation is complete, we can import the Bootstrap styles in our index.js file as follows:

import 'bootstrap/dist/css/bootstrap.min.css';

Customizing styles

While CSS frameworks provide a great starting point for styling our app, we may want to customize the styles to match our design preferences. To do this, we can create a new CSS file and write our custom styles. For example, we can create a file called styles.css in the src directory and add the following code:

/* styles.css */

body {
  font-family: 'Arial', sans-serif;
}

h1, h2, h3 {
  color: #333;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  margin-bottom: 1rem;
}

button {
  background-color: #333;
  color: #fff;
  padding: 0.5rem 1rem;
  border: none;
  cursor: pointer;
}

button:disabled {
  background-color: #999;
  cursor: not-allowed;
}

To apply these styles to our app, we need to import the styles.css file in our index.js file as follows:

import './styles.css';

Responsive design considerations

To ensure our app looks good on different screen sizes, we need to consider responsive design. We can use CSS media queries to apply different styles based on the screen size. For example, we can modify our styles.css file as follows:

/* styles.css */

/* Default styles */

body {
  font-family: 'Arial', sans-serif;
}

h1, h2, h3 {
  color: #333;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  margin-bottom: 1rem;
}

button {
  background-color: #333;
  color: #fff;
  padding: 0.5rem 1rem;
  border: none;
  cursor: pointer;
}

button:disabled {
  background-color: #999;
  cursor: not-allowed;
}

/* Responsive styles */

@media (max-width: 768px) {
  h1 {
    font-size: 1.5rem;
  }
  
  h2 {
    font-size: 1.25rem;
  }
  
  button {
    padding: 0.25rem 0.5rem;
  }
}

In this code, we apply different font sizes and button padding for screens with a maximum width of 768 pixels.

Conclusion

In this tutorial, we have built a recipe search app using React and the Edamam API. We started by setting up the project, installing necessary dependencies, and setting up API credentials. Then, we built the search component, handled user input, and made API requests. We also displayed the search results, implemented pagination, and added sorting and filtering options. Finally, we fetched recipe details, displayed recipe information, and implemented favorite functionality. Along the way, we also covered styling the app using CSS frameworks, customizing styles, and considering responsive design. With this app as a starting point, you can further customize and enhance it to meet your specific requirements.