5: Intermediate Practice Components
Three components built using the Intermediate concepts. Each one combines useEffect, search/filter, forms, and loading/error states into real interactive UIs.
1. GithubRepos
Concepts used: useEffect, fetch, loading/error/success states, component extraction, res.ok check
Fetches real data from the GitHub API and displays repos in a table. The RepoTable sub-component was extracted to keep the main component focused on data fetching logic.
import { useState, useEffect } from 'react'
interface Repo {
id: number
name: string
description: string | null
stargazers_count: number
html_url: string
}
export default function GithubRepos() {
const [repos, setRepos] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch("https://api.github.com/users/lxyhan/repos?sort=stars&per_page=10")
.then(res => {
if (!res.ok) throw new Error("Failed to Fetch from Github")
return res.json()
})
.then(repos => {
setRepos(repos)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
return (
<div>
{loading ? (
<p>loading</p>
) : error ? (
<p>{error}</p>
) : (
<RepoTable repos={repos} user="lxyhan"/>
)}
</div>
);
}
function RepoTable({repos, user} : {repos: Repo[], user: string}) {
return (
<table>
<caption>Github Repos for {user}</caption>
<thead>
<tr>
<td>Name</td>
<td>Description</td>
<td>Star Count</td>
</tr>
</thead>
<tbody>
{repos.map(repo =>
<tr key={repo.id}>
<td><a href={repo.html_url}>{repo.name}</a></td>
<td>{repo.description}</td>
<td>{repo.stargazers_count}</td>
</tr>
)}
</tbody>
</table>
)
}Live demo:
loading
Key takeaways:
res.okcheck:fetchdoesn’t reject on 404/500 — you must checkres.okyourself and throw.- Component extraction:
RepoTablehandles rendering;GithubReposhandles data. Separation of concerns. - Ternary chain:
loading ? ... : error ? ... : ...cleanly handles all three async states.
2. TechSearch
Concepts used: dual filter (text + category), derived state, controlled <select>, chained .filter()
A searchable, filterable list of technologies. Two independent filters combine — the text input narrows by name while the dropdown narrows by category.
import { useState } from 'react'
interface Tech {
id: number
name: string
category: string
description: string
}
const techStack: Tech[] = [
{ id: 1, name: 'React', category: 'Frontend', description: 'UI library for building component-based interfaces' },
{ id: 2, name: 'Vue', category: 'Frontend', description: 'Progressive framework for building UIs' },
{ id: 3, name: 'Node.js', category: 'Backend', description: 'JavaScript runtime built on V8 engine' },
{ id: 4, name: 'PostgreSQL', category: 'Database', description: 'Advanced open-source relational database' },
{ id: 5, name: 'TypeScript', category: 'Language', description: 'Typed superset of JavaScript' },
// ... plus more
]
const categories = ['All', 'Frontend', 'Backend', 'Database', 'Language']
export default function TechSearch() {
const [category, setCategory] = useState("All")
const [name, setName] = useState("")
const filtered = techStack
.filter(t => category === "All" || t.category === category)
.filter(t => t.name.toLowerCase().includes(name.toLowerCase()))
return (
<>
<div style={{display: "flex", gap: 10}}>
<select value={category} onChange={e => setCategory(e.target.value)}>
{categories.map(c => <option value={c} key={c}>{c}</option>)}
</select>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
</div>
<table>
<thead>
<tr><th>Name</th><th>Category</th><th>Description</th></tr>
</thead>
<tbody>
{filtered.map(t =>
<tr key={t.id}>
<td>{t.name}</td>
<td>{t.category}</td>
<td>{t.description}</td>
</tr>
)}
</tbody>
</table>
</>
);
}Live demo:
tech skills matching
| name | category | description |
|---|---|---|
| React | Frontend | UI library for building component-based interfaces |
| Vue | Frontend | Progressive framework for building UIs |
| Svelte | Frontend | Compiler that generates minimal framework-less code |
| Node.js | Backend | JavaScript runtime built on V8 engine |
| Express | Backend | Minimal web framework for Node.js |
| Django | Backend | High-level Python web framework |
| PostgreSQL | Database | Advanced open-source relational database |
| MongoDB | Database | Document-oriented NoSQL database |
| Redis | Database | In-memory data structure store |
| TypeScript | Language | Typed superset of JavaScript |
| Python | Language | General-purpose programming language |
| Rust | Language | Systems programming language focused on safety |
Key takeaways:
- Derived state:
filteredis not stored in state — it’s computed fromtechStack+category+nameon every render. - Chained
.filter(): Each filter is independent and composable. Adding a third filter is just another.filter()call. - Controlled
<select>: Same pattern as controlled<input>—value+onChange.
3. ContactForm
Concepts used: single state object, e.target.name pattern, validation, stale state fix
A contact form with validation that demonstrates the single-object form pattern and a real-world stale state bug fix.
import { useState } from 'react'
export default function ContactForm() {
const [data, setData] = useState({
name: "", email: "", message: ""
})
const [error, setError] = useState({
nameError: "", emailError: "",
emailMissingAtError: "", messageError: ""
})
const [submitted, setSubmitted] = useState(false)
const handleInput = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setData({...data, [e.target.name]: e.target.value})
}
const validate = () => {
// Key pattern: compute errors FIRST, then set state, then check.
// Reading `error` after setError() would be stale — state hasn't updated yet.
const newError = {
nameError: !data.name ? "Name is required" : "",
emailError: !data.email ? "Email is required" : "",
emailMissingAtError: (data.email && !data.email.includes("@"))
? "Email needs an @ symbol" : "",
messageError: !data.message ? "Message is required" : ""
};
setError(newError)
return !Object.values(newError).some(e => e)
}
const handleSubmit = () => {
if (validate()) { setSubmitted(true) }
}
return (
<div>
{submitted ? (
<h3 style={{color: "green"}}>Form Submitted Successfully!!</h3>
) : (
<form onSubmit={(e) => e.preventDefault()}>
<input name="name" value={data.name} placeholder="name" onChange={handleInput} />
{error.nameError && <p style={{color: "red"}}>{error.nameError}</p>}
<input name="email" value={data.email} placeholder="email" onChange={handleInput} />
{error.emailError && <p style={{color: "red"}}>{error.emailError}</p>}
{error.emailMissingAtError && <p>{error.emailMissingAtError}</p>}
<textarea name="message" value={data.message} placeholder="message" onChange={handleInput} />
{error.messageError && <p style={{color: "red"}}>{error.messageError}</p>}
<button type="submit" onClick={handleSubmit}>Submit</button>
</form>
)}
</div>
)
}Live demo:
Contact Form
Key takeaways:
- Stale state fix: The
validate()function computesnewErrorfirst, sets state, then returns based onnewError— noterror(which would be stale). e.target.namepattern: OnehandleInputhandles all fields. Each<input>has anameattribute matching the state key.Object.values().some(): A clean way to check if any error string is non-empty.