Skip to main content

Project Overview

The Food Order App from Section 18 is a comprehensive practice project that combines Context API, useReducer, HTTP requests, and form handling to create a realistic food ordering application with a backend.
Food Order Application

Learning Objectives

Context API

Manage global state with Context and useReducer

HTTP Requests

Fetch data and submit orders to a backend

Custom Hooks

Build reusable useHttp hook for data fetching

Form Handling

Handle user input and checkout flow

Key Features

Application Structure

App.jsx
import Cart from './components/Cart.jsx';
import Checkout from './components/Checkout.jsx';
import Header from './components/Header.jsx';
import Meals from './components/Meals.jsx';
import { CartContextProvider } from './store/CartContext.jsx';
import { UserProgressContextProvider } from './store/UserProgressContext.jsx';

function App() {
  return (
    <UserProgressContextProvider>
      <CartContextProvider>
        <Header />
        <Meals />
        <Cart />
        <Checkout />
      </CartContextProvider>
    </UserProgressContextProvider>
  );
}

export default App;

Core Features

Fetch and display available meals from the backend
Meals.jsx
import MealItem from './MealItem.jsx';
import useHttp from '../hooks/useHttp.js';
import Error from './Error.jsx';

const requestConfig = {};

export default function Meals() {
  const {
    data: loadedMeals,
    isLoading,
    error,
  } = useHttp('http://localhost:3000/meals', requestConfig, []);

  if (isLoading) {
    return <p className="center">Fetching meals...</p>;
  }

  if (error) {
    return <Error title="Failed to fetch meals" message={error} />;
  }

  return (
    <ul id="meals">
      {loadedMeals.map((meal) => (
        <MealItem key={meal.id} meal={meal} />
      ))}
    </ul>
  );
}
Add, remove, and manage items in the cart using useReducerThe cart uses a reducer to handle complex state updates:
  • Add items (increment quantity if already in cart)
  • Remove items (decrement quantity or remove)
  • Clear entire cart after checkout
Multi-step checkout with form validation and order submission
  • Collect customer information
  • Validate form inputs
  • Submit order to backend
  • Show success/error feedback

Context API Implementation

Cart Context with useReducer

CartContext.jsx
import { createContext, useReducer } from 'react';

const CartContext = createContext({
  items: [],
  addItem: (item) => {},
  removeItem: (id) => {},
  clearCart: () => {},
});

function cartReducer(state, action) {
  if (action.type === 'ADD_ITEM') {
    const existingCartItemIndex = state.items.findIndex(
      (item) => item.id === action.item.id
    );

    const updatedItems = [...state.items];

    if (existingCartItemIndex > -1) {
      // Item already in cart - increment quantity
      const existingItem = state.items[existingCartItemIndex];
      const updatedItem = {
        ...existingItem,
        quantity: existingItem.quantity + 1,
      };
      updatedItems[existingCartItemIndex] = updatedItem;
    } else {
      // New item - add with quantity 1
      updatedItems.push({ ...action.item, quantity: 1 });
    }

    return { ...state, items: updatedItems };
  }

  if (action.type === 'REMOVE_ITEM') {
    const existingCartItemIndex = state.items.findIndex(
      (item) => item.id === action.id
    );
    const existingCartItem = state.items[existingCartItemIndex];

    const updatedItems = [...state.items];

    if (existingCartItem.quantity === 1) {
      // Last item - remove from cart
      updatedItems.splice(existingCartItemIndex, 1);
    } else {
      // Decrement quantity
      const updatedItem = {
        ...existingCartItem,
        quantity: existingCartItem.quantity - 1,
      };
      updatedItems[existingCartItemIndex] = updatedItem;
    }

    return { ...state, items: updatedItems };
  }

  if (action.type === 'CLEAR_CART') {
    return { ...state, items: [] };
  }

  return state;
}

export function CartContextProvider({ children }) {
  const [cart, dispatchCartAction] = useReducer(cartReducer, { items: [] });

  function addItem(item) {
    dispatchCartAction({ type: 'ADD_ITEM', item });
  }

  function removeItem(id) {
    dispatchCartAction({ type: 'REMOVE_ITEM', id });
  }

  function clearCart() {
    dispatchCartAction({ type: 'CLEAR_CART' });
  }

  const cartContext = {
    items: cart.items,
    addItem,
    removeItem,
    clearCart
  };

  return (
    <CartContext.Provider value={cartContext}>{children}</CartContext.Provider>
  );
}

export default CartContext;
useReducer is ideal for complex state logic like shopping carts where multiple actions modify the same state object.

User Progress Context

Manages UI state for showing/hiding modals:
const UserProgressContext = createContext({
  progress: '', // '', 'cart', 'checkout'
  showCart: () => {},
  hideCart: () => {},
  showCheckout: () => {},
  hideCheckout: () => {},
});

Custom useHttp Hook

A reusable hook for making HTTP requests:
useHttp.js
import { useCallback, useEffect, useState } from 'react';

async function sendHttpRequest(url, config) {
  const response = await fetch(url, config);
  const resData = await response.json();

  if (!response.ok) {
    throw new Error(
      resData.message || 'Something went wrong, failed to send request.'
    );
  }

  return resData;
}

export default function useHttp(url, config, initialData) {
  const [data, setData] = useState(initialData);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

  function clearData() {
    setData(initialData);
  }

  const sendRequest = useCallback(
    async function sendRequest(data) {
      setIsLoading(true);
      try {
        const resData = await sendHttpRequest(url, { ...config, body: data });
        setData(resData);
      } catch (error) {
        setError(error.message || 'Something went wrong!');
      }
      setIsLoading(false);
    },
    [url, config]
  );

  useEffect(() => {
    // Automatically send GET requests
    if ((config && (config.method === 'GET' || !config.method)) || !config) {
      sendRequest();
    }
  }, [sendRequest, config]);

  return {
    data,
    isLoading,
    error,
    sendRequest,
    clearData
  };
}

Using the Hook

// Automatically fetches on mount
const { data, isLoading, error } = useHttp(
  'http://localhost:3000/meals',
  {},
  []
);

Component Architecture

Meal Display

MealItem.jsx
import { useContext } from 'react';
import { currencyFormatter } from '../util/formatting.js';
import Button from './UI/Button.jsx';
import CartContext from '../store/CartContext.jsx';

export default function MealItem({ meal }) {
  const cartCtx = useContext(CartContext);

  function handleAddMealToCart() {
    cartCtx.addItem(meal);
  }

  return (
    <li className="meal-item">
      <article>
        <img src={`http://localhost:3000/${meal.image}`} alt={meal.name} />
        <div>
          <h3>{meal.name}</h3>
          <p className="meal-item-price">
            {currencyFormatter.format(meal.price)}
          </p>
          <p className="meal-item-description">{meal.description}</p>
        </div>
        <p className="meal-item-actions">
          <Button onClick={handleAddMealToCart}>Add to Cart</Button>
        </p>
      </article>
    </li>
  );
}

Cart Display

Cart.jsx
import { useContext } from 'react';
import Modal from './UI/Modal.jsx';
import CartContext from '../store/CartContext.jsx';
import { currencyFormatter } from '../util/formatting.js';
import UserProgressContext from '../store/UserProgressContext.jsx';
import CartItem from './CartItem.jsx';

export default function Cart() {
  const cartCtx = useContext(CartContext);
  const userProgressCtx = useContext(UserProgressContext);

  const cartTotal = cartCtx.items.reduce(
    (totalPrice, item) => totalPrice + item.quantity * item.price,
    0
  );

  function handleCloseCart() {
    userProgressCtx.hideCart();
  }

  function handleGoToCheckout() {
    userProgressCtx.showCheckout();
  }

  return (
    <Modal
      className="cart"
      open={userProgressCtx.progress === 'cart'}
      onClose={handleCloseCart}
    >
      <h2>Your Cart</h2>
      <ul>
        {cartCtx.items.map((item) => (
          <CartItem
            key={item.id}
            name={item.name}
            quantity={item.quantity}
            price={item.price}
            onIncrease={() => cartCtx.addItem(item)}
            onDecrease={() => cartCtx.removeItem(item.id)}
          />
        ))}
      </ul>
      <p className="cart-total">{currencyFormatter.format(cartTotal)}</p>
      <p className="modal-actions">
        <Button textOnly onClick={handleCloseCart}>
          Close
        </Button>
        {cartCtx.items.length > 0 && (
          <Button onClick={handleGoToCheckout}>Go to Checkout</Button>
        )}
      </p>
    </Modal>
  );
}

Form Handling & Checkout

Form Structure

Checkout.jsx
export default function Checkout() {
  const cartCtx = useContext(CartContext);
  const userProgressCtx = useContext(UserProgressContext);

  const { data, isLoading, error, sendRequest, clearData } = useHttp(
    'http://localhost:3000/orders',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
    },
    null
  );

  const cartTotal = cartCtx.items.reduce(
    (totalPrice, item) => totalPrice + item.quantity * item.price,
    0
  );

  function handleSubmit(event) {
    event.preventDefault();

    const fd = new FormData(event.target);
    const customerData = Object.fromEntries(fd.entries());

    sendRequest(
      JSON.stringify({
        order: {
          items: cartCtx.items,
          customer: customerData,
        },
      })
    );
  }

  function handleFinish() {
    userProgressCtx.hideCheckout();
    cartCtx.clearCart();
    clearData();
  }

  // Show success message after order
  if (data && !error) {
    return (
      <Modal open={userProgressCtx.progress === 'checkout'}>
        <h2>Success!</h2>
        <p>Your order was submitted successfully.</p>
        <p className="modal-actions">
          <Button onClick={handleFinish}>Okay</Button>
        </p>
      </Modal>
    );
  }

  return (
    <Modal open={userProgressCtx.progress === 'checkout'}>
      <form onSubmit={handleSubmit}>
        <h2>Checkout</h2>
        <p>Total Amount: {currencyFormatter.format(cartTotal)}</p>
        
        <Input label="Full Name" type="text" id="name" />
        <Input label="E-Mail Address" type="email" id="email" />
        <Input label="Street" type="text" id="street" />
        
        <div className="control-row">
          <Input label="Postal Code" type="text" id="postal-code" />
          <Input label="City" type="text" id="city" />
        </div>

        {error && <Error title="Failed to submit order" message={error} />}

        <p className="modal-actions">
          <Button type="button" textOnly onClick={handleClose}>
            Close
          </Button>
          <Button>Submit Order</Button>
        </p>
        {isLoading && <p>Sending order data...</p>}
      </form>
    </Modal>
  );
}

Backend Integration

The project includes a Node.js backend:
cd backend
npm install
node app.js

API Endpoints

Returns array of available meals
[
  {
    "id": "m1",
    "name": "Mac & Cheese",
    "price": "8.99",
    "description": "Creamy cheddar cheese...",
    "image": "images/mac-and-cheese.jpg"
  }
]

Project Structure

18-practice-project-food-order/
├── src/
   ├── components/
   ├── UI/
   ├── Button.jsx          # Reusable button
   ├── Input.jsx           # Form input component
   └── Modal.jsx           # Modal dialog
   ├── Cart.jsx                # Shopping cart modal
   ├── CartItem.jsx            # Individual cart item
   ├── Checkout.jsx            # Checkout form
   ├── Error.jsx               # Error display
   ├── Header.jsx              # App header with cart button
   ├── MealItem.jsx            # Meal card
   └── Meals.jsx               # Meal list
   ├── hooks/
   └── useHttp.js              # Custom HTTP hook
   ├── store/
   ├── CartContext.jsx         # Cart state management
   └── UserProgressContext.jsx # UI state management
   ├── util/
   └── formatting.js           # Currency formatter
   ├── App.jsx                     # Main app component
   └── main.jsx                    # Entry point
├── backend/
   ├── app.js                      # Express server
   ├── data/
   └── available-meals.json    # Meal data
   └── public/
       └── images/                 # Meal images
└── package.json

Implementation Steps

1

Component Structure

Create Header, Meals, and basic layout components
2

Fetch Meals

Use useEffect to fetch meals from backend API
3

Cart Context

Implement CartContext with useReducer for state management
4

Add to Cart

Connect MealItem to CartContext to add items
5

Cart Modal

Build Modal component and display cart items
6

Cart Operations

Implement increase/decrease quantity and remove items
7

Checkout Form

Create form with input validation
8

Submit Order

Send POST request to backend with order data
9

Custom Hook

Extract HTTP logic into reusable useHttp hook
10

Error Handling

Add loading states and error messages

Key Patterns & Best Practices

Reducer Pattern for Cart

Cart operations are complex:
  • Check if item exists
  • Update quantity or add new
  • Handle removal vs. decrement
useReducer centralizes this logic and makes it testable.
Always create new arrays/objects:
// BAD - Mutates state
state.items.push(newItem);

// GOOD - Creates new array
return { ...state, items: [...state.items, newItem] };

Context Best Practices

Separate concerns: Use different contexts for different types of state (cart data vs. UI state).
// Cart data context
<CartContextProvider>
  {/* Business logic state */}
</CartContextProvider>

// UI state context  
<UserProgressContextProvider>
  {/* Modal visibility, navigation */}
</UserProgressContextProvider>

Common Challenges

Challenge: Handling edge cases when incrementing/decrementingSolution: Check if item exists and handle quantity === 1 specially
if (existingCartItem.quantity === 1) {
  // Remove completely
  updatedItems.splice(existingCartItemIndex, 1);
} else {
  // Just decrement
  updatedItem.quantity = existingItem.quantity - 1;
}
Challenge: Collecting all form inputs efficientlySolution: Use FormData API
function handleSubmit(event) {
  event.preventDefault();
  const fd = new FormData(event.target);
  const data = Object.fromEntries(fd.entries());
  // data = { name: '...', email: '...', etc }
}
Challenge: Multiple loading states (fetching meals, submitting order)Solution: Custom useHttp hook manages loading/error for each request

Testing Checklist

Key Takeaways

Context + Reducer

Powerful pattern for complex state management

Custom Hooks

Encapsulate and reuse logic across components

HTTP Integration

Handle loading, success, and error states

Form Handling

Use FormData API for efficient data collection

Next Steps

Redux

Learn alternative state management with Redux

React Query

Better way to handle server state

Form Validation

Deep dive into form handling

Testing

Learn to test React applications

Build docs developers (and LLMs) love