This synthesizes every layer from Modules 1 through 4: AuthContext for session state, Redux Toolkit's cartSlice and productsSlice for cross-cutting global state, a createAsyncThunk-driven product catalog, and Bootstrap for a fully responsive layout — nav bar, product grid, cards, alerts, and loading spinners.
Responsive Nav Bar: AuthContext + Cart Count
import { useAuth } from '../context/AuthContext'
import { useAppSelector } from '../app/hooks'
import { selectCartItemCount } from '../features/cart/cartSlice'
function NavBar() {
const { user, isAuthenticated, logout } = useAuth()
const cartItemCount = useAppSelector(selectCartItemCount)
return (
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<div className="container">
<a className="navbar-brand fw-bold" href="/">
<i className="bi bi-bag-check-fill me-2" />
RTK Store
</a>
<div className="d-flex align-items-center gap-3 ms-auto">
<a href="/cart" className="position-relative text-light text-decoration-none">
<i className="bi bi-cart3 fs-5" />
{cartItemCount > 0 && (
<span className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{cartItemCount}
<span className="visually-hidden">items in cart</span>
</span>
)}
</a>
{isAuthenticated ? (
<div className="dropdown">
<button
className="btn btn-outline-light btn-sm dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
>
{user?.name}
</button>
<ul className="dropdown-menu dropdown-menu-end">
<li>
<button className="dropdown-item" onClick={logout}>
Sign Out
</button>
</li>
</ul>
</div>
) : (
<a href="/login" className="btn btn-outline-light btn-sm">
Sign In
</a>
)}
</div>
</div>
</nav>
)
}
export default NavBarProduct Grid: Async Thunk + Typed Add-to-Cart
import { useEffect } from 'react'
import { useAppDispatch, useAppSelector } from '../app/hooks'
import {
fetchProducts,
selectProducts,
selectProductsStatus,
selectProductsError
} from '../features/products/productsSlice'
import { itemAdded } from '../features/cart/cartSlice'
function ProductCatalog() {
const dispatch = useAppDispatch()
const products = useAppSelector(selectProducts)
const status = useAppSelector(selectProductsStatus)
const error = useAppSelector(selectProductsError)
useEffect(() => {
if (status === 'idle') {
dispatch(fetchProducts('all'))
}
}, [status, dispatch])
function handleAddToCart(productId: string) {
const product = products.find((p) => p.id === productId)
if (!product) return
dispatch(
itemAdded({
productId: product.id,
name: product.name,
price: product.price
})
)
}
if (status === 'loading' || status === 'idle') {
return (
<div className="d-flex flex-column align-items-center py-5">
<div className="spinner-border text-primary mb-3" role="status">
<span className="visually-hidden">Loading products…</span>
</div>
<p className="text-muted">Loading catalog…</p>
</div>
)
}
if (status === 'failed') {
return (
<div className="alert alert-danger d-flex align-items-center gap-2" role="alert">
<i className="bi bi-exclamation-triangle-fill" />
<span>{error}</span>
</div>
)
}
if (products.length === 0) {
return <div className="alert alert-info">No products available right now.</div>
}
return (
<div className="row g-4">
{products.map((product) => (
<div key={product.id} className="col-12 col-sm-6 col-lg-4">
<div className="card h-100 shadow-sm">
<img
src={product.imageUrl}
className="card-img-top"
alt={product.name}
style={{ objectFit: 'cover', height: 180 }}
/>
<div className="card-body d-flex flex-column">
<h5 className="card-title">{product.name}</h5>
<span className="badge text-bg-secondary align-self-start mb-2">
{product.category}
</span>
<p className="card-text fw-bold fs-5 mt-auto">
${product.price.toFixed(2)}
</p>
<button
type="button"
className="btn btn-primary"
onClick={() => handleAddToCart(product.id)}
>
<i className="bi bi-cart-plus me-1" />
Add to Cart
</button>
</div>
</div>
</div>
))}
</div>
)
}
export default ProductCatalogAssembling the App
import { AuthProvider } from './context/AuthContext'
import NavBar from './components/NavBar'
import ProductCatalog from './components/ProductCatalog'
function App() {
return (
<AuthProvider>
<NavBar />
<main className="container py-4">
<h1 className="mb-4 fw-bold">Product Catalog</h1>
<ProductCatalog />
</main>
</AuthProvider>
)
}
export default AppNote the layering: AuthProvider (Context, Module 3) wraps the whole tree for session state, while NavBar and ProductCatalog both read from the Redux store (Module 4) via useAppSelector — two different state mechanisms coexisting deliberately, each used for the kind of state it fits.
| Tool | Ideal use case | Re-render cost | Key TypeScript pattern |
|---|---|---|---|
| useState | Component-local UI state: form fields, toggles, hover state | Only the owning component re-renders | Explicit generic for object shapes: useState<ShippingAddress>({...}) |
| useEffect | Syncing with an external system: fetch, subscriptions, DOM APIs | N/A — runs a side effect, not a render trigger by itself | Cleanup function return type: () => void; AbortController for cancellable fetches |
| useMemo / useCallback | Preserving reference equality for React.memo children or hook dependency arrays | No re-render cost of their own; skips re-renders in dependents | useCallback<(id: string) => void>(...) or let inference handle it from the function literal |
| Context API | Low-frequency data read broadly: auth session, theme, locale | Every consumer re-renders on any value change — no selector layer | Default to undefined, throw in a custom hook (useAuth) if accessed outside the Provider |
| Redux Toolkit (slice) | Synchronous cross-cutting state: cart, UI flags shared across features | Scoped via useAppSelector — only re-renders on the selected slice changing | PayloadAction<T> on every reducer; Immer allows mutation-style updates safely |
| Redux Toolkit (createAsyncThunk) | Asynchronous data flows: API fetches with loading/error lifecycle | Same as above — status field drives conditional rendering | createAsyncThunk<Returned, ThunkArg>(...) + typed extraReducers via the builder API |
Capstone Takeaways
- A real application layers state deliberately: Context for broad, low-frequency data (auth); Redux Toolkit for high-frequency, cross-cutting data (cart, product catalog) — not one tool for everything.
- Bootstrap's interactive components (dropdowns, offcanvas, modals) work via data-bs-* attributes and the bundled JS, independent of React state — don't build custom open/close state for them unless you need programmatic control.
- Every dispatch call and every useAppSelector call is fully typed end-to-end, from the store's inferred RootState through each slice's action creators — this is what "complete type safety" means in practice, not just typing props.
- The cheat sheet's re-render cost column is the deciding factor most teams skip — picking Context for fast-changing state, or Redux for a single component's private toggle, are the two most common architecture missteps in production React + TS codebases.