Build a Next.js Interactive Web-Based Recipe App

Written by

in

Tired of endlessly scrolling through recipe websites, battling intrusive ads, and struggling to find that one dish you remember from last week? In today’s digital age, we’re constantly seeking efficient and user-friendly solutions. Imagine having all your favorite recipes readily available, searchable, and beautifully displayed in a custom-built application. That’s precisely what we’ll achieve by building an interactive, web-based recipe app using Next.js, a powerful React framework.

Why Build a Recipe App with Next.js?

Next.js offers a compelling combination of features that make it ideal for this project:

  • Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js excels at optimizing website performance and SEO. SSR renders pages on the server, improving initial load times, while SSG pre-renders pages at build time, resulting in lightning-fast delivery. This is crucial for a recipe app, as fast loading times are vital for user experience and search engine rankings.
  • React-Based: Leveraging React’s component-based architecture allows for reusable and maintainable code. This makes it easier to manage and update your recipe app as it grows.
  • Built-in Routing: Next.js simplifies navigation with its file-system-based routing, making it easy to create pages for different recipes, categories, and user profiles.
  • API Routes: Easily create backend APIs to handle data fetching, user authentication, and other server-side logic.
  • Developer Experience: Next.js provides a fantastic developer experience with features like hot module replacement, automatic code splitting, and optimized image handling.

Project Overview: What We’ll Build

Our recipe app will feature the following functionalities:

  • Recipe Display: Showcasing recipes with titles, ingredients, instructions, and images.
  • Search Functionality: Allowing users to search for recipes by name, ingredients, or keywords.
  • Recipe Categories: Organizing recipes into categories like “Breakfast,” “Dinner,” “Desserts,” etc.
  • User Interface: A clean and intuitive design for a pleasant user experience.
  • Responsive Design: Ensuring the app works flawlessly on all devices (desktops, tablets, and smartphones).

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js and npm (or yarn): These are essential for managing project dependencies and running the development server. Download them from nodejs.org.
  • A Code Editor: Visual Studio Code (VS Code) is highly recommended, but you can use any editor you prefer.
  • Basic Knowledge of HTML, CSS, and JavaScript: Familiarity with these web technologies is crucial for understanding the code.

Step-by-Step Guide: Building the Recipe App

1. Setting Up the Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app recipe-app

This command creates a new directory called “recipe-app” and installs all the necessary dependencies. Navigate into the project directory:

cd recipe-app

Now, start the development server:

npm run dev

Open your browser and go to http://localhost:3000. You should see the default Next.js welcome page. Congratulations, your Next.js project is set up!

2. Project Structure and File Organization

Next.js uses a specific file structure. Here’s how we’ll organize our project:

recipe-app/
├── pages/
│   ├── _app.js          # Global styles and layout
│   ├── index.js         # Home page (recipe list)
│   └── [recipe].js      # Recipe detail page (dynamic route)
├── components/
│   ├── RecipeCard.js    # Component for displaying a single recipe
│   ├── RecipeList.js    # Component for displaying a list of recipes
│   ├── SearchBar.js     # Component for the search bar
│   └── CategoryMenu.js  # Component for category selection
├── styles/
│   ├── globals.css      # Global styles
│   └── RecipeStyles.module.css # Styles for recipe-related components
├── public/
│   └── ...              # Images, icons, etc.
└── ...

This structure helps keep our code organized and maintainable. We’ll create these files and folders as we progress through the tutorial.

3. Creating the Recipe Data

For this tutorial, we’ll use a simple JSON file to store our recipe data. In a real-world application, you’d likely fetch this data from a database or API.

Create a file named “recipes.json” in the root of your project and add some sample recipe data. Here’s an example:

[
  {
    "id": 1,
    "title": "Spaghetti Carbonara",
    "category": "Dinner",
    "image": "/images/carbonara.jpg",
    "ingredients": [
      "Spaghetti",
      "Eggs",
      "Pancetta",
      "Parmesan Cheese",
      "Black Pepper"
    ],
    "instructions": [
      "Cook spaghetti according to package directions.",
      "Fry pancetta until crispy.",
      "Whisk eggs with Parmesan and pepper.",
      "Combine pasta, pancetta, and egg mixture. Toss quickly.",
      "Serve immediately."
    ]
  },
  {
    "id": 2,
    "title": "Chocolate Chip Cookies",
    "category": "Dessert",
    "image": "/images/cookies.jpg",
    "ingredients": [
      "Flour",
      "Butter",
      "Sugar",
      "Chocolate Chips",
      "Eggs"
    ],
    "instructions": [
      "Preheat oven to 375°F (190°C).",
      "Cream butter and sugar.",
      "Add eggs and mix.",
      "Combine dry ingredients.",
      "Add chocolate chips.",
      "Bake for 10-12 minutes."
    ]
  }
]

Important: Create an “images” folder inside the “public” directory and add some sample images for your recipes. Make sure the image paths in your `recipes.json` file match the location of your images.

4. Creating the RecipeCard Component

The `RecipeCard` component will display a single recipe’s information. Create a file named `RecipeCard.js` inside the `components` folder and add the following code:

import Image from 'next/image';
import styles from '../styles/RecipeStyles.module.css';

function RecipeCard({ recipe }) {
  return (
    <div>
      
      <h3>{recipe.title}</h3>
      <p><b>Category:</b> {recipe.category}</p>
      <a href="{`/recipes/${recipe.id}`}">View Recipe</a>
    </div>
  );
}

export default RecipeCard;

This component uses the `Image` component from Next.js for optimized image handling. We also import a styles module (`RecipeStyles.module.css`) to style the card. We’ll create this file later.

5. Creating the RecipeList Component

The `RecipeList` component will display a list of `RecipeCard` components. Create a file named `RecipeList.js` inside the `components` folder:

import RecipeCard from './RecipeCard';
import styles from '../styles/RecipeStyles.module.css';

function RecipeList({ recipes }) {
  return (
    <div>
      {recipes.map((recipe) => (
        
      ))}
    </div>
  );
}

export default RecipeList;

This component takes an array of recipes as a prop and renders a `RecipeCard` for each one.

6. Creating the SearchBar Component

The `SearchBar` component will allow users to search for recipes. Create a file named `SearchBar.js` inside the `components` folder:

import { useState } from 'react';
import styles from '../styles/RecipeStyles.module.css';

function SearchBar({ onSearch }) {
  const [searchTerm, setSearchTerm] = useState('');

  const handleChange = (event) => {
    setSearchTerm(event.target.value);
    onSearch(event.target.value);
  };

  return (
    <div>
      
    </div>
  );
}

export default SearchBar;

This component uses the `useState` hook to manage the search term and calls the `onSearch` prop function (passed from the parent component) whenever the search term changes.

7. Creating the CategoryMenu Component

The `CategoryMenu` component will allow users to filter recipes by category. Create a file named `CategoryMenu.js` inside the `components` folder:

import styles from '../styles/RecipeStyles.module.css';

function CategoryMenu({ categories, onCategorySelect }) {
  return (
    <div>
      {categories.map((category) => (
        <button> onCategorySelect(category)}>
          {category}
        </button>
      ))}
    </div>
  );
}

export default CategoryMenu;

This component takes an array of categories and an `onCategorySelect` prop function. It renders a button for each category, and when a button is clicked, it calls the `onCategorySelect` function with the selected category.

8. Styling the Components (RecipeStyles.module.css)

Now, let’s add some styles to our components. Create a file named `RecipeStyles.module.css` inside the `styles` folder and add the following CSS:

.recipeCard {
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  text-align: center;
}

.recipeCard img {
  border-radius: 8px;
  margin-bottom: 8px;
}

.recipeCard a {
  display: inline-block;
  margin-top: 8px;
  padding: 8px 16px;
  background-color: #0070f3;
  color: white;
  text-decoration: none;
  border-radius: 4px;
}

.recipeList {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
}

.searchBar {
  margin-bottom: 20px;
}

.searchBar input {
  width: 100%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 16px;
}

.categoryMenu {
  margin-bottom: 20px;
}

.categoryMenu button {
  padding: 10px 16px;
  margin-right: 10px;
  background-color: #eee;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.categoryMenu button:hover {
  background-color: #ddd;
}

This CSS provides basic styling for our recipe cards, search bar, and category menu. Feel free to customize these styles to match your desired aesthetic.

9. Building the Home Page (index.js)

Now, let’s build the home page, which will display the list of recipes. Open `pages/index.js` and replace the existing code with the following:

import { useState, useEffect } from 'react';
import RecipeList from '../components/RecipeList';
import SearchBar from '../components/SearchBar';
import CategoryMenu from '../components/CategoryMenu';
import styles from '../styles/RecipeStyles.module.css';

function HomePage() {
  const [recipes, setRecipes] = useState([]);
  const [filteredRecipes, setFilteredRecipes] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedCategory, setSelectedCategory] = useState(null);
  const [categories, setCategories] = useState([]);

  useEffect(() => {
    async function fetchRecipes() {
      const response = await fetch('/recipes.json');
      const data = await response.json();
      setRecipes(data);
      setFilteredRecipes(data);

      const uniqueCategories = [...new Set(data.map(recipe => recipe.category))];
      setCategories(uniqueCategories);
    }
    fetchRecipes();
  }, []);

  useEffect(() => {
    let newFilteredRecipes = recipes;

    if (searchTerm) {
      newFilteredRecipes = newFilteredRecipes.filter(recipe =>
        recipe.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
        recipe.ingredients.some(ingredient => ingredient.toLowerCase().includes(searchTerm.toLowerCase()))
      );
    }

    if (selectedCategory) {
      newFilteredRecipes = newFilteredRecipes.filter(recipe => recipe.category === selectedCategory);
    }

    setFilteredRecipes(newFilteredRecipes);

  }, [searchTerm, selectedCategory, recipes]);

  const handleSearch = (term) => {
    setSearchTerm(term);
  };

  const handleCategorySelect = (category) => {
    setSelectedCategory(category);
  };

  return (
    <div>
      <h1>Recipe App</h1>
      
      
      
    </div>
  );
}

export default HomePage;

Let’s break down what this code does:

  • Imports: Imports the necessary components (RecipeList, SearchBar, CategoryMenu) and the `useState` and `useEffect` hooks.
  • State Variables:
    • `recipes`: Stores the original recipe data.
    • `filteredRecipes`: Stores the recipes that are currently displayed, based on search and category filters.
    • `searchTerm`: Stores the search term entered by the user.
    • `selectedCategory`: Stores the currently selected category (e.g., “Dinner”).
    • `categories`: Stores an array of unique categories from the recipe data.
  • useEffect (Fetch Recipes): This `useEffect` hook runs once when the component mounts. It fetches the recipe data from `recipes.json`, sets the `recipes` state, initializes `filteredRecipes`, and extracts unique categories for the `CategoryMenu`.
  • useEffect (Filter Recipes): This `useEffect` hook runs whenever `searchTerm`, `selectedCategory`, or `recipes` changes. It filters the `recipes` array based on the current `searchTerm` and `selectedCategory`, and updates the `filteredRecipes` state.
  • handleSearch Function: Updates the `searchTerm` state when the user types in the search bar.
  • handleCategorySelect Function: Updates the `selectedCategory` state when the user selects a category.
  • JSX: Renders the `SearchBar`, `CategoryMenu`, and `RecipeList` components, passing the necessary props.

This code fetches the recipe data, allows users to search by recipe title and ingredients, and filter by category. It then displays the filtered recipes using the `RecipeList` component.

10. Building the Recipe Detail Page ([recipe].js)

Now, let’s create the recipe detail page, which will display the full details of a single recipe. Next.js uses file-system-based routing, so we’ll create a dynamic route using a file named `[recipe].js` inside the `pages` directory. This means that any file name within square brackets becomes a dynamic parameter.

Create a file named `[recipe].js` inside the `pages` folder and add the following code:

import { useRouter } from 'next/router';
import Image from 'next/image';
import styles from '../styles/RecipeStyles.module.css';

function RecipeDetail({ recipe }) {
  const router = useRouter();
  const { recipe: recipeId } = router.query;

  if (!recipe) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <h2>{recipe.title}</h2>
      
      <p><b>Category:</b> {recipe.category}</p>
      <h3>Ingredients:</h3>
      <ul>
        {recipe.ingredients.map((ingredient, index) => (
          <li>{ingredient}</li>
        ))}
      </ul>
      <h3>Instructions:</h3>
      <ol>
        {recipe.instructions.map((instruction, index) => (
          <li>{instruction}</li>
        ))}
      </ol>
      <button> router.back()}>Back to Recipes</button>
    </div>
  );
}

export async function getStaticPaths() {
  // Retrieve all recipe IDs from your data (e.g., from recipes.json)
  const fs = require('fs');
  const path = require('path');

  const filePath = path.join(process.cwd(), 'recipes.json');
  const jsonData = fs.readFileSync(filePath, 'utf8');
  const recipes = JSON.parse(jsonData);

  const paths = recipes.map((recipe) => ({
    params: {
      recipe: recipe.id.toString(),
    },
  }));

  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  // Fetch recipe data based on the ID
  const fs = require('fs');
  const path = require('path');

  const filePath = path.join(process.cwd(), 'recipes.json');
  const jsonData = fs.readFileSync(filePath, 'utf8');
  const recipes = JSON.parse(jsonData);

  const recipe = recipes.find((recipe) => recipe.id.toString() === params.recipe);

  return {
    props: {
      recipe: recipe || null,
    },
  };
}

export default RecipeDetail;

This code does the following:

  • Imports: Imports `useRouter` from `next/router` to access the route parameters and `Image` from ‘next/image’ for optimized image handling.
  • useRouter Hook: Uses the `useRouter` hook to get the route parameters. The `recipeId` is extracted from the `router.query`.
  • Loading State: Displays “Loading…” while the recipe data is being fetched.
  • JSX: Renders the recipe details, including the title, image, ingredients, and instructions.
  • Back Button: Includes a button that uses `router.back()` to navigate back to the previous page (the recipe list).
  • getStaticPaths: This function is used to specify the paths that need to be pre-rendered at build time. It fetches all the recipe IDs from `recipes.json` and creates paths for each recipe detail page.
  • getStaticProps: This function fetches the data for a specific recipe based on its ID. It reads the `recipes.json` file, finds the matching recipe, and passes it as a prop to the component.

11. Styling the Recipe Detail Page

Add the following CSS to your `RecipeStyles.module.css` file to style the recipe detail page:

.recipeDetail {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  margin-bottom: 20px;
}

.recipeDetail h2 {
  font-size: 2em;
  margin-bottom: 10px;
}

.recipeDetail h3 {
  font-size: 1.5em;
  margin-top: 20px;
  margin-bottom: 10px;
}

.recipeDetail ul, .recipeDetail ol {
  margin-left: 20px;
}

.recipeDetail button {
  padding: 10px 20px;
  background-color: #0070f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1em;
  margin-top: 20px;
}

.recipeDetail button:hover {
  background-color: #0056b3;
}

12. Implementing Global Styles and Layout (_app.js)

The `_app.js` file in the `pages` directory is a special file in Next.js that allows you to customize the application’s behavior. We can use it to apply global styles and define a layout that’s consistent across all pages.

Open `pages/_app.js` and replace the existing code with the following:

import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <div>
      
    </div>
  );
}

export default MyApp;

This code imports the global styles from `styles/globals.css` and renders the `Component` (which is the page component) with its props. We’ve also added a container class to wrap the content, which we’ll style in `globals.css`.

Add the following CSS to your `styles/globals.css` file:

html, body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
  background-color: #f4f4f4;
}

.container {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
  background-color: white;
  min-height: 100vh;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

h1 {
  text-align: center;
  margin-bottom: 20px;
}

13. Testing and Refinement

Now, run your Next.js development server ( `npm run dev` ) and test your recipe app. Navigate to the home page ( http://localhost:3000 ) and verify that:

  • The recipe list is displayed correctly.
  • You can search for recipes by title and ingredients.
  • You can filter recipes by category.
  • Clicking on a recipe card takes you to the recipe detail page.
  • The recipe detail page displays the correct information.
  • The back button works as expected.

Make any necessary adjustments to the code, styles, and data to refine your app. Consider adding more features, such as:

  • User Authentication: Allow users to create accounts and save their favorite recipes.
  • Recipe Creation: Enable users to add their own recipes.
  • Database Integration: Connect your app to a database (e.g., PostgreSQL, MongoDB) to store and retrieve recipe data.
  • Advanced Search: Implement more advanced search filters (e.g., by cooking time, dietary restrictions).
  • Pagination: Implement pagination for long lists of recipes.

Key Takeaways

  • Next.js for Performance: Next.js provides excellent performance benefits through SSR and SSG, making your recipe app fast and SEO-friendly.
  • Component-Based Architecture: React’s component-based structure allows for reusable and maintainable code.
  • Dynamic Routing: Next.js simplifies routing with its file-system-based approach, making it easy to create different pages.
  • API Routes: Next.js makes it easy to create API endpoints for backend functionality.
  • SEO Optimization: Next.js is designed with SEO in mind, helping your recipe app rank well in search results.

FAQ

1. How do I deploy this app?

You can deploy your Next.js app to various platforms, including Vercel (recommended, as it’s built by the Next.js team), Netlify, and other hosting providers. Vercel offers a seamless deployment process directly from your Git repository.

2. Can I use a database instead of a JSON file?

Yes, absolutely! Using a database (like PostgreSQL, MongoDB, or Firebase) is recommended for real-world applications. You’ll need to install the appropriate database client library and modify your code to fetch and save data from the database.

3. How can I add images to my recipes?

You can store images in the `public/images` directory and reference them in your `recipes.json` data. Make sure to use the Next.js `Image` component for optimized image handling. You can also allow users to upload images and store them in a cloud storage service like AWS S3 or Cloudinary.

4. How do I handle errors?

Implement error handling in your `useEffect` hooks and API routes. Use `try…catch` blocks to catch errors and display user-friendly error messages. You can also use a dedicated error handling library or service for more robust error management.

5. What are some other Next.js features I can explore?

Explore other Next.js features such as API routes, image optimization, and advanced routing techniques to enhance your recipe app. Consider using TypeScript for better type safety and code maintainability.

Building a web-based recipe app with Next.js is a rewarding project that combines modern web development techniques with a practical, everyday application. By following this guide, you’ve gained a solid understanding of Next.js fundamentals, including routing, data fetching, component creation, and styling. The skills you’ve learned here can be applied to a wide range of web projects. As you continue to develop your app, remember to experiment, iterate, and refine your code. Embrace the power of Next.js and create a recipe app that’s not only functional but also a joy to use. The journey of building your own recipe app is just the beginning; the possibilities are endless, and the culinary world, at your fingertips.