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