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.
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
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 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 >
);
}
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
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:
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
GET Request (Auto)
POST Request (Manual)
// Automatically fetches on mount
const { data , isLoading , error } = useHttp (
'http://localhost:3000/meals' ,
{},
[]
);
const { data , isLoading , error , sendRequest , clearData } = useHttp (
'http://localhost:3000/orders' ,
{
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
},
null
);
// Call manually when needed
function handleSubmit ( orderData ) {
sendRequest ( JSON . stringify ( orderData ));
}
Component Architecture
Meal Display
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
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 >
);
}
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"
}
]
Submits a new order {
"order" : {
"items" : [
{ "id" : "m1" , "quantity" : 2 , "price" : "8.99" }
],
"customer" : {
"name" : "John Doe" ,
"email" : "john@example.com" ,
"street" : "123 Main St" ,
"postal-code" : "12345" ,
"city" : "New York"
}
}
}
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
Component Structure
Create Header, Meals, and basic layout components
Fetch Meals
Use useEffect to fetch meals from backend API
Cart Context
Implement CartContext with useReducer for state management
Add to Cart
Connect MealItem to CartContext to add items
Cart Modal
Build Modal component and display cart items
Cart Operations
Implement increase/decrease quantity and remove items
Checkout Form
Create form with input validation
Submit Order
Send POST request to backend with order data
Custom Hook
Extract HTTP logic into reusable useHttp hook
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 speciallyif ( existingCartItem . quantity === 1 ) {
// Remove completely
updatedItems . splice ( existingCartItemIndex , 1 );
} else {
// Just decrement
updatedItem . quantity = existingItem . quantity - 1 ;
}
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