Single Page Application (SPA)
Learning Objectives
- By the end of this project, you will be able to:
- - Build complex single-page applications
- - Implement advanced state management
- - Create seamless routing and navigation
- - Integrate with external APIs
- - Design responsive, modern UIs
- - Optimize application performance
- - Handle complex user interactions
- - Deploy SPAs to production
Capstone Project 2: Single Page Application (SPA)
Project Overview
Build a production-ready Single Page Application (SPA) using a modern framework (React or Vue). This capstone project will demonstrate mastery of frontend development, state management, routing, API integration, and responsive design.
Learning Objectives
By the end of this project, you will be able to:
- Build complex single-page applications
- Implement advanced state management
- Create seamless routing and navigation
- Integrate with external APIs
- Design responsive, modern UIs
- Optimize application performance
- Handle complex user interactions
- Deploy SPAs to production
Project Requirements
Core Features
-
Multiple Routes & Navigation
- At least 5-7 different pages/routes
- Protected routes
- Dynamic routes with parameters
- Nested routes (optional)
-
State Management
- Global state management (Context API or Redux)
- Local component state
- State persistence
- Complex state interactions
-
API Integration
- Connect to external APIs or mock backend
- Handle loading states
- Error handling
- Data caching (optional)
-
User Interface
- Modern, polished design
- Responsive (mobile, tablet, desktop)
- Smooth animations and transitions
- Accessible components
-
Advanced Features
- Search and filtering
- Pagination or infinite scroll
- Form validation
- Image handling
- Real-time updates (optional)
Technical Requirements
- Framework: React or Vue.js
- State Management: Context API, Redux, or Vuex
- Routing: React Router or Vue Router
- Styling: CSS Modules, Styled Components, or Tailwind CSS
- API: Axios or Fetch API
- Build Tool: Vite, Create React App, or Vue CLI
- Performance: Code splitting, lazy loading
- Responsive: Mobile-first design
Project Ideas
Choose one of these or create your own:
Option 1: Movie Database App
- Browse movies and TV shows
- Search functionality
- Movie details with cast and reviews
- Watchlist/favorites
- User ratings
Option 2: Recipe Finder App
- Search recipes by ingredients
- Recipe details with instructions
- Save favorite recipes
- Meal planning
- Shopping list generation
Option 3: News Aggregator
- Multiple news sources
- Category filtering
- Article reading view
- Bookmark articles
- Search functionality
Option 4: Music Discovery App
- Browse artists and albums
- Playlist creation
- Search songs
- Recently played
- Favorite tracks
Option 5: Job Board Application
- Browse job listings
- Filter by location, type, salary
- Save jobs
- Application tracking
- Company profiles
Recommended Project: Movie Database App
We'll use a Movie Database App as our example. You can adapt this to any of the options above.
Project Structure
movie-database-app/
├── public/
│ └── index.html
├── src/
│ ├── components/
│ │ ├── Layout/
│ │ │ ├── Header.jsx
│ │ │ ├── Navigation.jsx
│ │ │ └── Footer.jsx
│ │ ├── Movie/
│ │ │ ├── MovieCard.jsx
│ │ │ ├── MovieList.jsx
│ │ │ ├── MovieDetail.jsx
│ │ │ └── MovieSearch.jsx
│ │ ├── Common/
│ │ │ ├── Loading.jsx
│ │ │ ├── Error.jsx
│ │ │ ├── Pagination.jsx
│ │ │ └── FilterBar.jsx
│ │ └── Watchlist/
│ │ └── WatchlistButton.jsx
│ ├── pages/
│ │ ├── Home.jsx
│ │ ├── Movies.jsx
│ │ ├── MovieDetail.jsx
│ │ ├── Watchlist.jsx
│ │ ├── Search.jsx
│ │ └── About.jsx
│ ├── context/
│ │ ├── MovieContext.jsx
│ │ └── WatchlistContext.jsx
│ ├── hooks/
│ │ ├── useMovies.js
│ │ ├── useLocalStorage.js
│ │ └── useDebounce.js
│ ├── services/
│ │ └── api.js
│ ├── utils/
│ │ └── helpers.js
│ ├── styles/
│ │ ├── App.css
│ │ └── components.css
│ ├── App.jsx
│ └── index.js
└── package.json
Step-by-Step Implementation
Phase 1: Project Setup
# Create React app with Vite
npm create vite@latest movie-database-app -- --template react
cd movie-database-app
npm install
# Install dependencies
npm install react-router-dom axios date-fns
npm install --save-dev @vitejs/plugin-react
# Start development server
npm run dev
Phase 2: API Service
// src/services/api.js
import axios from 'axios';
// Using The Movie Database (TMDB) API
// Sign up at https://www.themoviedb.org/ to get API key
const API_KEY = process.env.REACT_APP_TMDB_API_KEY || 'your-api-key';
const BASE_URL = 'https://api.themoviedb.org/3';
const IMAGE_BASE_URL = 'https://image.tmdb.org/t/p/w500';
const api = axios.create({
baseURL: BASE_URL,
params: {
api_key: API_KEY
}
});
export const moviesAPI = {
// Get popular movies
getPopularMovies: (page = 1) =>
api.get('/movie/popular', { params: { page } }),
// Get top rated movies
getTopRatedMovies: (page = 1) =>
api.get('/movie/top_rated', { params: { page } }),
// Get now playing movies
getNowPlayingMovies: (page = 1) =>
api.get('/movie/now_playing', { params: { page } }),
// Get upcoming movies
getUpcomingMovies: (page = 1) =>
api.get('/movie/upcoming', { params: { page } }),
// Get movie by ID
getMovieById: (id) =>
api.get(`/movie/${id}`),
// Search movies
searchMovies: (query, page = 1) =>
api.get('/search/movie', { params: { query, page } }),
// Get movie credits
getMovieCredits: (id) =>
api.get(`/movie/${id}/credits`),
// Get movie reviews
getMovieReviews: (id, page = 1) =>
api.get(`/movie/${id}/reviews`, { params: { page } }),
// Get similar movies
getSimilarMovies: (id, page = 1) =>
api.get(`/movie/${id}/similar`, { params: { page } })
};
export const getImageUrl = (path) => {
return path ? `${IMAGE_BASE_URL}${path}` : 'https://via.placeholder.com/500';
};
export default api;
Phase 3: Custom Hooks
// src/hooks/useMovies.js
import { useState, useEffect } from 'react';
import { moviesAPI } from '../services/api';
export function useMovies(endpoint, page = 1) {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
loadMovies();
}, [endpoint, page]);
const loadMovies = async () => {
try {
setLoading(true);
setError(null);
const response = await moviesAPI[endpoint](page);
setMovies(response.data.results);
setTotalPages(response.data.total_pages);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return { movies, loading, error, totalPages, refetch: loadMovies };
}
export function useMovie(id) {
const [movie, setMovie] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [credits, setCredits] = useState(null);
const [reviews, setReviews] = useState([]);
useEffect(() => {
if (id) {
loadMovie();
}
}, [id]);
const loadMovie = async () => {
try {
setLoading(true);
setError(null);
const [movieRes, creditsRes, reviewsRes] = await Promise.all([
moviesAPI.getMovieById(id),
moviesAPI.getMovieCredits(id),
moviesAPI.getMovieReviews(id)
]);
setMovie(movieRes.data);
setCredits(creditsRes.data);
setReviews(reviewsRes.data.results);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return { movie, credits, reviews, loading, error };
}
export function useSearchMovies(query, page = 1) {
const [movies, setMovies] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [totalPages, setTotalPages] = useState(1);
useEffect(() => {
if (query.trim()) {
searchMovies();
} else {
setMovies([]);
}
}, [query, page]);
const searchMovies = async () => {
try {
setLoading(true);
setError(null);
const response = await moviesAPI.searchMovies(query, page);
setMovies(response.data.results);
setTotalPages(response.data.total_pages);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return { movies, loading, error, totalPages };
}
// src/hooks/useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// src/hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
Phase 4: Context Providers
// src/context/WatchlistContext.jsx
import { createContext, useContext } from 'react';
import { useLocalStorage } from '../hooks/useLocalStorage';
const WatchlistContext = createContext();
export function WatchlistProvider({ children }) {
const [watchlist, setWatchlist] = useLocalStorage('watchlist', []);
const addToWatchlist = (movie) => {
if (!watchlist.find(m => m.id === movie.id)) {
setWatchlist([...watchlist, movie]);
}
};
const removeFromWatchlist = (movieId) => {
setWatchlist(watchlist.filter(m => m.id !== movieId));
};
const isInWatchlist = (movieId) => {
return watchlist.some(m => m.id === movieId);
};
return (
<WatchlistContext.Provider
value={{
watchlist,
addToWatchlist,
removeFromWatchlist,
isInWatchlist
}}
>
{children}
</WatchlistContext.Provider>
);
}
export function useWatchlist() {
const context = useContext(WatchlistContext);
if (!context) {
throw new Error('useWatchlist must be used within WatchlistProvider');
}
return context;
}
Phase 5: Components
// src/components/Movie/MovieCard.jsx
import { Link } from 'react-router-dom';
import { getImageUrl } from '../../services/api';
import { useWatchlist } from '../../context/WatchlistContext';
import './MovieCard.css';
function MovieCard({ movie }) {
const { addToWatchlist, removeFromWatchlist, isInWatchlist } = useWatchlist();
const inWatchlist = isInWatchlist(movie.id);
const handleWatchlistToggle = (e) => {
e.preventDefault();
if (inWatchlist) {
removeFromWatchlist(movie.id);
} else {
addToWatchlist(movie);
}
};
return (
<div className="movie-card">
<Link to={`/movie/${movie.id}`} className="movie-link">
<div className="movie-image-container">
<img
src={getImageUrl(movie.poster_path)}
alt={movie.title}
className="movie-image"
/>
<div className="movie-overlay">
<div className="movie-rating">
⭐ {movie.vote_average?.toFixed(1)}
</div>
</div>
</div>
<div className="movie-info">
<h3 className="movie-title">{movie.title}</h3>
<p className="movie-date">
{movie.release_date ? new Date(movie.release_date).getFullYear() : 'N/A'}
</p>
</div>
</Link>
<button
onClick={handleWatchlistToggle}
className={`watchlist-btn ${inWatchlist ? 'active' : ''}`}
title={inWatchlist ? 'Remove from watchlist' : 'Add to watchlist'}
>
{inWatchlist ? '✓' : '+'}
</button>
</div>
);
}
export default MovieCard;
// src/components/Movie/MovieList.jsx
import { lazy, Suspense } from 'react';
import MovieCard from './MovieCard';
import Loading from '../Common/Loading';
import './MovieList.css';
const MovieCardLazy = lazy(() => import('./MovieCard'));
function MovieList({ movies, loading }) {
if (loading) {
return <Loading />;
}
if (movies.length === 0) {
return (
<div className="no-movies">
<p>No movies found</p>
</div>
);
}
return (
<div className="movie-list">
<Suspense fallback={<Loading />}>
{movies.map(movie => (
<MovieCard key={movie.id} movie={movie} />
))}
</Suspense>
</div>
);
}
export default MovieList;
// src/components/Movie/MovieDetail.jsx
import { useMovie } from '../../hooks/useMovies';
import { getImageUrl } from '../../services/api';
import { format } from 'date-fns';
import Loading from '../Common/Loading';
import Error from '../Common/Error';
import './MovieDetail.css';
function MovieDetail({ movieId }) {
const { movie, credits, reviews, loading, error } = useMovie(movieId);
if (loading) {
return <Loading />;
}
if (error) {
return <Error message={error} />;
}
if (!movie) {
return <Error message="Movie not found" />;
}
return (
<div className="movie-detail">
<div className="movie-detail-hero">
<img
src={getImageUrl(movie.backdrop_path)}
alt={movie.title}
className="movie-backdrop"
/>
<div className="movie-detail-content">
<div className="movie-detail-poster">
<img
src={getImageUrl(movie.poster_path)}
alt={movie.title}
/>
</div>
<div className="movie-detail-info">
<h1>{movie.title}</h1>
<div className="movie-meta">
<span>⭐ {movie.vote_average?.toFixed(1)}</span>
<span>•</span>
<span>{movie.runtime} min</span>
<span>•</span>
<span>{format(new Date(movie.release_date), 'yyyy')}</span>
</div>
<div className="movie-genres">
{movie.genres?.map(genre => (
<span key={genre.id} className="genre-tag">
{genre.name}
</span>
))}
</div>
<p className="movie-overview">{movie.overview}</p>
</div>
</div>
</div>
{credits && credits.cast && (
<section className="movie-cast">
<h2>Cast</h2>
<div className="cast-list">
{credits.cast.slice(0, 10).map(actor => (
<div key={actor.id} className="cast-item">
<img
src={getImageUrl(actor.profile_path)}
alt={actor.name}
/>
<p className="actor-name">{actor.name}</p>
<p className="character-name">{actor.character}</p>
</div>
))}
</div>
</section>
)}
{reviews.length > 0 && (
<section className="movie-reviews">
<h2>Reviews</h2>
<div className="reviews-list">
{reviews.map(review => (
<div key={review.id} className="review-item">
<div className="review-header">
<strong>{review.author}</strong>
<span>{format(new Date(review.created_at), 'MMM dd, yyyy')}</span>
</div>
<p>{review.content}</p>
</div>
))}
</div>
</section>
)}
</div>
);
}
export default MovieDetail;
// src/components/Movie/MovieSearch.jsx
import { useState, useEffect } from 'react';
import { useSearchMovies } from '../../hooks/useMovies';
import { useDebounce } from '../../hooks/useDebounce';
import MovieList from './MovieList';
import Pagination from '../Common/Pagination';
import './MovieSearch.css';
function MovieSearch() {
const [searchQuery, setSearchQuery] = useState('');
const [page, setPage] = useState(1);
const debouncedQuery = useDebounce(searchQuery, 500);
const { movies, loading, error, totalPages } = useSearchMovies(debouncedQuery, page);
useEffect(() => {
setPage(1);
}, [debouncedQuery]);
return (
<div className="movie-search">
<div className="search-container">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search movies..."
className="search-input"
/>
</div>
{debouncedQuery && (
<>
<h2>Search Results for "{debouncedQuery}"</h2>
<MovieList movies={movies} loading={loading} />
{totalPages > 1 && (
<Pagination
currentPage={page}
totalPages={totalPages}
onPageChange={setPage}
/>
)}
</>
)}
</div>
);
}
export default MovieSearch;
// src/components/Common/Pagination.jsx
import './Pagination.css';
function Pagination({ currentPage, totalPages, onPageChange }) {
const getPageNumbers = () => {
const pages = [];
const maxVisible = 5;
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let end = Math.min(totalPages, start + maxVisible - 1);
if (end - start < maxVisible - 1) {
start = Math.max(1, end - maxVisible + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
};
return (
<div className="pagination">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="pagination-btn"
>
Previous
</button>
<div className="pagination-numbers">
{getPageNumbers().map(page => (
<button
key={page}
onClick={() => onPageChange(page)}
className={`pagination-btn ${currentPage === page ? 'active' : ''}`}
>
{page}
</button>
))}
</div>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="pagination-btn"
>
Next
</button>
</div>
);
}
export default Pagination;
Phase 6: Pages
// src/pages/Home.jsx
import { Link } from 'react-router-dom';
import { useMovies } from '../hooks/useMovies';
import MovieList from '../components/Movie/MovieList';
import './Home.css';
function Home() {
const { movies, loading } = useMovies('getPopularMovies', 1);
return (
<div className="home">
<section className="hero">
<h1>Discover Amazing Movies</h1>
<p>Explore the latest and greatest films</p>
<Link to="/movies" className="btn btn-primary">
Browse Movies
</Link>
</section>
<section className="featured-section">
<h2>Popular Movies</h2>
<MovieList movies={movies} loading={loading} />
<div className="section-footer">
<Link to="/movies" className="btn btn-link">
View All Movies →
</Link>
</div>
</section>
</div>
);
}
export default Home;
// src/pages/Movies.jsx
import { useState } from 'react';
import { useMovies } from '../hooks/useMovies';
import MovieList from '../components/Movie/MovieList';
import FilterBar from '../components/Common/FilterBar';
import Pagination from '../components/Common/Pagination';
import './Movies.css';
const FILTERS = {
popular: 'getPopularMovies',
topRated: 'getTopRatedMovies',
nowPlaying: 'getNowPlayingMovies',
upcoming: 'getUpcomingMovies'
};
function Movies() {
const [filter, setFilter] = useState('popular');
const [page, setPage] = useState(1);
const { movies, loading, totalPages } = useMovies(FILTERS[filter], page);
return (
<div className="movies-page">
<h1>Movies</h1>
<FilterBar
filters={[
{ key: 'popular', label: 'Popular' },
{ key: 'topRated', label: 'Top Rated' },
{ key: 'nowPlaying', label: 'Now Playing' },
{ key: 'upcoming', label: 'Upcoming' }
]}
activeFilter={filter}
onFilterChange={setFilter}
/>
<MovieList movies={movies} loading={loading} />
{totalPages > 1 && (
<Pagination
currentPage={page}
totalPages={totalPages}
onPageChange={setPage}
/>
)}
</div>
);
}
export default Movies;
// src/pages/MovieDetail.jsx
import { useParams } from 'react-router-dom';
import MovieDetail from '../components/Movie/MovieDetail';
import './MovieDetailPage.css';
function MovieDetailPage() {
const { id } = useParams();
return (
<div className="movie-detail-page">
<MovieDetail movieId={id} />
</div>
);
}
export default MovieDetailPage;
// src/pages/Watchlist.jsx
import { useWatchlist } from '../context/WatchlistContext';
import MovieList from '../components/Movie/MovieList';
import './Watchlist.css';
function Watchlist() {
const { watchlist } = useWatchlist();
return (
<div className="watchlist-page">
<h1>My Watchlist</h1>
{watchlist.length === 0 ? (
<div className="empty-watchlist">
<p>Your watchlist is empty</p>
<p>Start adding movies to your watchlist!</p>
</div>
) : (
<MovieList movies={watchlist} loading={false} />
)}
</div>
);
}
export default Watchlist;
// src/pages/Search.jsx
import MovieSearch from '../components/Movie/MovieSearch';
import './Search.css';
function Search() {
return (
<div className="search-page">
<h1>Search Movies</h1>
<MovieSearch />
</div>
);
}
export default Search;
Phase 7: App Component with Routing
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { WatchlistProvider } from './context/WatchlistContext';
import Layout from './components/Layout/Layout';
import Loading from './components/Common/Loading';
import './App.css';
// Lazy load pages for code splitting
const Home = lazy(() => import('./pages/Home'));
const Movies = lazy(() => import('./pages/Movies'));
const MovieDetail = lazy(() => import('./pages/MovieDetail'));
const Watchlist = lazy(() => import('./pages/Watchlist'));
const Search = lazy(() => import('./pages/Search'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<WatchlistProvider>
<BrowserRouter>
<Layout>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/movies" element={<Movies />} />
<Route path="/movie/:id" element={<MovieDetail />} />
<Route path="/watchlist" element={<Watchlist />} />
<Route path="/search" element={<Search />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Layout>
</BrowserRouter>
</WatchlistProvider>
);
}
export default App;
Phase 8: Layout Component
// src/components/Layout/Layout.jsx
import Header from './Header';
import Footer from './Footer';
import './Layout.css';
function Layout({ children }) {
return (
<div className="layout">
<Header />
<main className="main-content">
{children}
</main>
<Footer />
</div>
);
}
export default Layout;
// src/components/Layout/Header.jsx
import { Link, useLocation } from 'react-router-dom';
import { useWatchlist } from '../../context/WatchlistContext';
import './Header.css';
function Header() {
const location = useLocation();
const { watchlist } = useWatchlist();
const isActive = (path) => location.pathname === path;
return (
<header className="header">
<div className="header-container">
<Link to="/" className="logo">
🎬 MovieDB
</Link>
<nav className="nav">
<Link
to="/"
className={`nav-link ${isActive('/') ? 'active' : ''}`}
>
Home
</Link>
<Link
to="/movies"
className={`nav-link ${isActive('/movies') ? 'active' : ''}`}
>
Movies
</Link>
<Link
to="/search"
className={`nav-link ${isActive('/search') ? 'active' : ''}`}
>
Search
</Link>
<Link
to="/watchlist"
className={`nav-link ${isActive('/watchlist') ? 'active' : ''}`}
>
Watchlist
{watchlist.length > 0 && (
<span className="badge">{watchlist.length}</span>
)}
</Link>
<Link
to="/about"
className={`nav-link ${isActive('/about') ? 'active' : ''}`}
>
About
</Link>
</nav>
</div>
</header>
);
}
export default Header;
Features Implementation
State Management
- Watchlist Context: Global watchlist state
- Custom Hooks: Reusable data fetching hooks
- Local Storage: Persist watchlist
- Optimistic Updates: Immediate UI feedback
Routing
- React Router: Client-side routing
- Lazy Loading: Code splitting for performance
- Dynamic Routes: Movie detail pages
- Navigation: Smooth page transitions
API Integration
- TMDB API: External movie database
- Error Handling: Graceful error handling
- Loading States: User feedback
- Caching: Reduce API calls
Responsive Design
- Mobile-First: Start with mobile
- Flexible Layouts: Grid and flexbox
- Breakpoints: Multiple screen sizes
- Touch-Friendly: Large tap targets
Performance Optimization
Code Splitting
// Lazy load components
const MovieDetail = lazy(() => import('./pages/MovieDetail'));
Image Optimization
// Use different image sizes
const getImageUrl = (path, size = 'w500') => {
return path ? `https://image.tmdb.org/t/p/${size}${path}` : placeholder;
};
Memoization
import { useMemo } from 'react';
const filteredMovies = useMemo(() => {
return movies.filter(movie => /* filter logic */);
}, [movies, filter]);
Testing Your Application
Manual Testing Checklist
- [ ] Navigate between all routes
- [ ] Search functionality works
- [ ] Add/remove from watchlist
- [ ] Pagination works
- [ ] Filter movies
- [ ] View movie details
- [ ] Responsive on mobile
- [ ] Loading states display
- [ ] Error handling works
- [ ] Performance is good
Deployment
Build for Production
npm run build
Deploy to Vercel
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel
# Set environment variables
vercel env add REACT_APP_TMDB_API_KEY
Deploy to Netlify
# Install Netlify CLI
npm install -g netlify-cli
# Deploy
netlify deploy --prod
# Set environment variables in Netlify dashboard
Exercise: Single Page Application
Instructions:
- Choose your project idea
- Set up React/Vue project
- Plan your routes and state
- Implement all features
- Optimize performance
- Test thoroughly
- Deploy to production
Timeline: 2-3 weeks recommended
Evaluation Criteria
Your project will be evaluated on:
-
Functionality (30%)
- All features work correctly
- Smooth navigation
- No critical bugs
-
State Management (20%)
- Proper state organization
- Efficient updates
- State persistence
-
Routing (15%)
- All routes work
- Navigation is smooth
- Protected routes (if applicable)
-
API Integration (15%)
- Proper API usage
- Error handling
- Loading states
-
Design & UX (15%)
- Modern, polished design
- Responsive layout
- Good user experience
-
Performance (5%)
- Fast load times
- Smooth interactions
- Optimized code
Common Issues and Solutions
Issue: Slow performance
Solution: Implement code splitting, lazy loading, and memoization.
Issue: API rate limiting
Solution: Implement caching and debouncing for search.
Issue: Routing not working
Solution: Ensure BrowserRouter wraps the app and routes are correct.
Quiz: SPA Concepts
-
SPA:
- A) Single Page Application
- B) Multiple pages
- C) Both
- D) Neither
-
State management:
- A) Important for SPAs
- B) Not important
- C) Both
- D) Neither
-
Routing:
- A) Client-side navigation
- B) Server-side navigation
- C) Both
- D) Neither
-
Code splitting:
- A) Improves performance
- B) Doesn't improve performance
- C) Both
- D) Neither
-
Responsive design:
- A) Works on all devices
- B) Desktop only
- C) Both
- D) Neither
Answers:
- A) Single Page Application
- A) Important for SPAs
- A) Client-side navigation
- A) Improves performance
- A) Works on all devices
Key Takeaways
- SPA Architecture: Single page with client-side routing
- State Management: Critical for complex apps
- Routing: Smooth navigation without page reloads
- API Integration: Connect to external services
- Performance: Optimize with code splitting and lazy loading
- Best Practice: Clean code, good UX, responsive design
Next Steps
Congratulations! You've built a Single Page Application. You now know:
- How to build complex SPAs
- How to manage state effectively
- How to implement routing
- How to integrate with APIs
- How to optimize performance
What's Next?
- Capstone Project 3: Real-Time Application
- Learn WebSocket integration
- Build real-time features
- Create advanced applications
Capstone Project completed! You've demonstrated mastery of modern frontend development!