Time to combine everything you've learned! In this project, you'll build a complete "Cosmic Star Catalog" application that fetches data from an API, handles loading and error states, enables searching, and supports pagination.
Your application should include:
fetch() with async/await1// Component structure:
2// App
3// ├── SearchBar (search input)
4// ├── useFetch (custom hook)
5// ├── LoadingSpinner / ErrorMessage / EmptyState
6// ├── StarList (results list)
7// │ └── StarCard (single result)
8// └── Pagination (page navigation)1import React, { useState, useEffect } from 'react';
2
3// Custom Hook - useFetch
4function useFetch(url) {
5 const [data, setData] = useState(null);
6 const [loading, setLoading] = useState(true);
7 const [error, setError] = useState(null);
8
9 useEffect(() => {
10 if (!url) return;
11 const controller = new AbortController();
12
13 async function fetchData() {
14 try {
15 setLoading(true);
16 setError(null);
17 const response = await fetch(url, {
18 signal: controller.signal
19 });
20 if (!response.ok) {
21 throw new Error(\`HTTP \${response.status}\`);
22 }
23 const result = await response.json();
24 setData(result);
25 } catch (err) {
26 if (err.name !== 'AbortError') {
27 setError(err.message);
28 }
29 } finally {
30 setLoading(false);
31 }
32 }
33
34 fetchData();
35 return () => controller.abort();
36 }, [url]);
37
38 const refetch = () => {
39 setData(null);
40 setLoading(true);
41 setError(null);
42 // Trigger re-fetch via state change
43 };
44
45 return { data, loading, error, refetch };
46}
47
48// Helper components
49function LoadingSpinner() {
50 return (
51 <div style={{ textAlign: 'center', padding: '40px' }}>
52 <div className="spinner"></div>
53 <p>Scanning the galaxy...</p>
54 </div>
55 );
56}
57
58function ErrorMessage({ message, onRetry }) {
59 return (
60 <div className="error-panel">
61 <h3>Connection lost!</h3>
62 <p>{message}</p>
63 <button onClick={onRetry}>Retry scan</button>
64 </div>
65 );
66}
67
68function EmptyState({ query }) {
69 return (
70 <div className="empty-panel">
71 <p>No results {query ? \`for "\${query}"\` : ''}</p>
72 <p>Try a different query</p>
73 </div>
74 );
75}
76
77function StarCard({ star }) {
78 return (
79 <div className="star-card">
80 <h3>{star.name}</h3>
81 <p>Height: {star.height} cm</p>
82 <p>Mass: {star.mass} kg</p>
83 <p>Hair color: {star.hair_color}</p>
84 <p>Birth year: {star.birth_year}</p>
85 </div>
86 );
87}
88
89function Pagination({ page, totalPages, onPageChange }) {
90 return (
91 <div className="pagination">
92 <button
93 onClick={() => onPageChange(page - 1)}
94 disabled={page === 1}
95 >
96 Previous
97 </button>
98 <span>Page {page} of {totalPages}</span>
99 <button
100 onClick={() => onPageChange(page + 1)}
101 disabled={page >= totalPages}
102 >
103 Next
104 </button>
105 </div>
106 );
107}
108
109// Main application
110function App() {
111 const [searchQuery, setSearchQuery] = useState('');
112 const [debouncedQuery, setDebouncedQuery] = useState('');
113 const [page, setPage] = useState(1);
114
115 // Debounce search
116 useEffect(() => {
117 const timer = setTimeout(() => {
118 setDebouncedQuery(searchQuery);
119 setPage(1); // Reset page on new search
120 }, 500);
121 return () => clearTimeout(timer);
122 }, [searchQuery]);
123
124 // Build URL with parameters
125 const url = debouncedQuery
126 ? \`https://swapi.dev/api/people/?search=\${debouncedQuery}&page=\${page}\`
127 : \`https://swapi.dev/api/people/?page=\${page}\`;
128
129 const { data, loading, error } = useFetch(url);
130 const totalPages = data ? Math.ceil(data.count / 10) : 1;
131
132 return (
133 <div className="app">
134 <h1>Cosmic Star Catalog</h1>
135
136 <input
137 type="text"
138 value={searchQuery}
139 onChange={e => setSearchQuery(e.target.value)}
140 placeholder="Search the galaxy..."
141 className="search-input"
142 />
143
144 {loading && <LoadingSpinner />}
145
146 {error && (
147 <ErrorMessage
148 message={error}
149 onRetry={() => setPage(p => p)}
150 />
151 )}
152
153 {!loading && !error && data && (
154 <>
155 {data.results.length === 0 ? (
156 <EmptyState query={debouncedQuery} />
157 ) : (
158 <>
159 <p>Found: {data.count} results</p>
160 <div className="star-grid">
161 {data.results.map(star => (
162 <StarCard key={star.name} star={star} />
163 ))}
164 </div>
165 <Pagination
166 page={page}
167 totalPages={totalPages}
168 onPageChange={setPage}
169 />
170 </>
171 )}
172 </>
173 )}
174 </div>
175 );
176}
177
178export default App;This project combines all the techniques from this module: fetch API, loading/error/empty states, useEffect, custom hook useFetch, debouncing, and pagination. Try building it yourself, then compare with the solution above!