Forms in Real-World Use: a React Practical Example
Forms in Real-World Use
~10 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
To wrap up Phase 3, let's finally connect ReviewForm to our real page – submitted reviews get collected and displayed right below it. Persistent storage (our own server + database) doesn't arrive until Phase 5 – for now, reviews live only in React state and disappear again after a page reload.
Updating App.jsx: wiring in ReviewForm
Add state for collected reviews and a callback function that ReviewForm calls on submit, in src/App.jsx:
import { useMemo, useRef, useState } from 'react';
import ProductCard from './components/ProductCard';
import ReviewForm from './components/ReviewForm';
import { useAuth } from './context/AuthContext';
import { useDocumentTitle } from './hooks/useDocumentTitle';
const SAMPLE_PRODUCTS = [
{ sku: 'boots-01', name: 'Hiking Boots', price: 89.99, imageUrl: 'https://picsum.photos/seed/1/300' },
{ sku: 'backpack-01', name: 'Trekking Backpack', price: 59.5, imageUrl: 'https://picsum.photos/seed/2/300' },
{ sku: 'jacket-01', name: 'Rain Jacket', price: 74.0, imageUrl: 'https://picsum.photos/seed/3/300' },
];
function App() {
const { user, login, logout } = useAuth();
const [query, setQuery] = useState('');
const [reviews, setReviews] = useState([]);
const searchInputRef = useRef(null);
useDocumentTitle(`Product Catalog (${SAMPLE_PRODUCTS.length} products)`);
const filtered = SAMPLE_PRODUCTS.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase())
);
const mostExpensiveProduct = useMemo(() => {
return filtered.reduce(
(max, p) => (p.price > (max?.price ?? 0) ? p : max),
null
);
}, [filtered]);
function handleReviewSubmit(review) {
setReviews([...reviews, { ...review, id: Date.now() }]);
}
return (
<div className="app">
<header className="app-header">
<h1>Product Catalog</h1>
{user ? (
<p>Logged in as {user.username} <button onClick={logout}>Log out</button></p>
) : (
<button onClick={() => login('Jane Doe')}>Log in</button>
)}
</header>
<input
ref={searchInputRef}
type="text"
placeholder="Search products..."
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
{mostExpensiveProduct && (
<p>Most expensive product: {mostExpensiveProduct.name} (${mostExpensiveProduct.price.toFixed(2)})</p>
)}
<div className="product-grid">
{filtered.map((product) => (
<ProductCard
key={product.sku}
name={product.name}
price={product.price}
imageUrl={product.imageUrl}
onSelect={() => alert(product.name + ' tapped')}
/>
))}
</div>
<section className="reviews-section">
<h2>Review a product</h2>
<ReviewForm onSubmitReview={handleReviewSubmit} />
{reviews.length === 0 ? (
<p>No reviews yet.</p>
) : (
<ul className="review-list">
{reviews.map((review) => (
<li key={review.id}>
<strong>{review.name}</strong> – {review.rating} stars
<p>{review.comment}</p>
</li>
))}
</ul>
)}
</section>
</div>
);
}
export default App;Project structure after this chapter
produktkatalog-web/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.jsx
├── App.jsx (updated: ReviewForm wired in)
├── index.css
├── context/
│ └── AuthContext.jsx
├── hooks/
│ └── useDocumentTitle.js
└── components/
├── ProductCard.jsx
└── ReviewForm.jsx ← NEWLY wired insetReviews([...reviews, {{ ...review, id: Date.now() }}]) follows the same pattern as every state update in this tutorial: a NEW array instead of reviews.push(...). Date.now() as a simple, unique id is enough for our purposes – key={{review.id}} in the list relies on exactly this (see chapter 7).
Save, fill out the form, and submit: your review appears instantly below the form, without the page reloading. That wraps up Phase 3 (Forms) – next, we'll build several real sub-pages with routing.
Tipp: In Phase 5 (API integration, starting chapter 25) we'll build our own small server plus a MySQL database, so reviews survive a page reload – until then, local React state is entirely sufficient to understand and test the form itself.