We use cookies to enhance your experience on the site
CodeWorlds

Main Project: Cosmic Star Catalog

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.

Project Requirements

Your application should include:

  1. Fetching data from an API - use
    fetch()
    with async/await
  2. Loading state - display a spinner or skeleton during loading
  3. Error handling - show an error message with a retry option
  4. Empty data state - handle the situation when there are no results
  5. Search - filter results by name with debouncing
  6. Pagination - navigation between result pages
  7. Custom hook useFetch - extract the data fetching logic

Application Architecture

1// 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)

Implementation

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!

Go to CodeWorlds