| |

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.ok check: fetch doesn’t reject on 404/500 — you must check res.ok yourself and throw.
  • Component extraction: RepoTable handles rendering; GithubRepos handles 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

namecategorydescription
ReactFrontendUI library for building component-based interfaces
VueFrontendProgressive framework for building UIs
SvelteFrontendCompiler that generates minimal framework-less code
Node.jsBackendJavaScript runtime built on V8 engine
ExpressBackendMinimal web framework for Node.js
DjangoBackendHigh-level Python web framework
PostgreSQLDatabaseAdvanced open-source relational database
MongoDBDatabaseDocument-oriented NoSQL database
RedisDatabaseIn-memory data structure store
TypeScriptLanguageTyped superset of JavaScript
PythonLanguageGeneral-purpose programming language
RustLanguageSystems programming language focused on safety

Key takeaways:

  • Derived state: filtered is not stored in state — it’s computed from techStack + category + name on 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 computes newError first, sets state, then returns based on newError — not error (which would be stale).
  • e.target.name pattern: One handleInput handles all fields. Each <input> has a name attribute matching the state key.
  • Object.values().some(): A clean way to check if any error string is non-empty.