Skip to content

breakingthebot/github-dashboard-react-query

Repository files navigation

GitHub Dashboard

Test

Search any GitHub user's public repos, with real caching, background refetch, loading/error states, and infinite-scroll-style pagination — powered by TanStack Query.

Stack

  • React 19 + TanStack Query (@tanstack/react-query)
  • Vite (dev server + build)
  • Vitest (unit tests)
  • Plain CSS (no framework, no CSS-in-JS)
  • GitHub REST API (unauthenticated — no token, no backend)

Setup

  1. Install Node.js 22 or newer.
  2. Clone this repo.
  3. npm install

Environment Variables

None required to run. See .env.example:

  • VITE_GITHUB_API_BASE_URL — optionally overrides the GitHub API base URL (defaults to https://api.github.com). Mainly useful for pointing at a mock server in tests/CI, not needed for normal use.

Running Locally

  • npm run dev — start the dev server (prints a local URL).
  • npm test — run the Vitest suite.
  • npm run build — production build to dist/.
  • npm run preview — serve the production build locally.

Deployed

Live at github-dashboard-react-query.vercel.app. Deployed via vercel CLI (not the dashboard), so the deploy history stays in version control. vercel.json rewrites every path to index.html so a direct link or refresh on a repo detail page (/owner/repo) works correctly instead of 404ing. The GitHub repo is connected for automatic redeploys on push to main.

Architecture Notes

Type a GitHub username, see their public repos, sorted by most recently updated, loaded 10 at a time with a "Load more" button. The search box is live — there's no Search button. Typing updates a debounced value (src/hooks/useDebouncedValue.js, 400ms) and only the settled result feeds the query, so pausing while typing triggers one fetch instead of one per keystroke. Clearing the box shows a plain "type a username" prompt rather than firing a request with an empty username. The part that actually matters here isn't the UI — it's that every fetch goes through TanStack Query (useInfiniteQuery) instead of a plain useEffect + fetch. That gets several things for free that would otherwise be hand-rolled and easy to get subtly wrong:

  1. Caching — searching the same username again within a minute (staleTime in src/config/queryClient.js) reuses every page already fetched instead of spending another request. This matters because GitHub's unauthenticated REST API only allows 60 requests/hour per IP.
  2. Loading vs. refetchingisLoading is only true on the very first fetch for a given query key; isFetching covers every fetch including silent background refetches; isFetchingNextPage is specifically true while a "Load more" click is in flight, so the button can say "Loading more…" without disturbing the rest of the page.
  3. Structured error stateisError/error come straight from the query, no manual try/catch state juggling in the component.
  4. Page accumulation without hand-rolled stateuseInfiniteQuery's data.pages array holds every page fetched so far under one cache entry; the component just flattens it. Switching usernames gets a brand-new query key, so there's no stale "page 3" left over from a previous search to reset by hand.
  5. No loading flash on a new searchplaceholderData: keepPreviousData keeps the previous username's results on screen (dimmed, via isPlaceholderData) while a brand-new username's first page loads, instead of the list going blank between one search and the next.

There's deliberately no GitHub API token anywhere: this is a static client-side app, and any token embedded in it would be visible to anyone who opens dev tools — not actually secret. The trade-off is GitHub's public rate limit (60 requests/hour/IP), which src/services/githubService.js treats as a real, user-facing error ("GitHub API rate limit exceeded. Try again in a few minutes.") rather than a silent failure or a generic "something went wrong."

"Load more" doesn't guess whether a next page exists from the current page count — it reads the real answer from GitHub's Link response header (src/utils/parseLinkHeader.js), so the button is replaced with "That's every public repository." exactly when the API says there's nothing more to fetch.

Repo cards

Each card shows more than just a name and description: language, stars, forks, open issues, license, and last-updated date, plus topic pills and a small "Fork"/"Archived" badge when applicable. None of this needs an extra request — GitHub's repo-list endpoint already returns every one of these fields on the same response fetchUserRepos() already fetches, so showing more here is purely a rendering change in RepoCard.jsx.

Repo detail page (/:owner/:repoName)

Clicking a repo's name goes to a dedicated page fetching three things in parallel: full repo details, a language breakdown, and the 5 most recent commits — three independent useQuery calls, each with its own loading/error state, rather than one big combined fetch. That independence matters: if GitHub's commits endpoint is slow or rate-limited, the repo details and language bar still render — a failed/slow query never blocks the rest of the page. The language breakdown converts raw byte counts into percentages (src/utils/computeLanguagePercentages.js) and assigns each language a consistent color via a deterministic hash (src/utils/colorForLanguage.js), so the same language always renders the same color without hardcoding a lookup table for every language GitHub recognizes.

File-by-file

  • src/services/githubService.js — every GitHub API call: fetchUserRepos(), fetchRepoDetails(), fetchRepoLanguages(), fetchRepoCommits(). All share one internal assertGithubResponseOk() helper so the 404/403/other-error handling only has to be right in one place. Takes an injectable fetchFn parameter so tests never make real network calls.
  • src/utils/parseLinkHeader.js — parses GitHub's Link header into a { rel: url } map. Pure function, no dependencies.
  • src/utils/computeLanguagePercentages.js — converts a repo's raw language byte counts into sorted percentages.
  • src/utils/colorForLanguage.js — deterministically maps a language name to a consistent color.
  • src/config/queryClient.js — the shared QueryClient instance and its defaults (staleTime, retry behavior).
  • src/hooks/useGithubRepos.js — wraps fetchUserRepos in useInfiniteQuery, keyed by ["github-repos", username, perPage]. getNextPageParam reads each page's hasNextPage flag; the next page number is just "however many pages fetched so far, plus one," since pages are always requested in order with no gaps. placeholderData: keepPreviousData keeps the previous query key's pages visible (as isPlaceholderData) while a new key's first page is in flight.
  • src/hooks/useDebouncedValue.js — generic hook that returns a value only after it stops changing for a given delay. Used to turn live keystrokes into a settled search term.
  • src/hooks/useRepoDetail.js — three hooks (useRepoDetails, useRepoLanguages, useRepoCommits) for the detail page, each wrapping one useQuery call.
  • src/components/RepoDashboard.jsx — the search page's stateful container. Owns the raw search input, calls the hook, flattens data.pages into one repo list, and renders the right thing for loading/error/success.
  • src/components/RepoDetailPage.jsx — the /:owner/:repoName route's container, composing the three detail hooks and the two presentational pieces below.
  • src/components/RepoLanguages.jsx, RepoCommitList.jsx — presentational, render a query's data/loading/error state for the languages bar and commit list respectively.
  • src/components/SearchForm.jsx, RepoList.jsx, RepoCard.jsx, LoadMoreButton.jsx — presentational pieces with no data-fetching logic of their own. SearchForm is a fully controlled live-typing input (the parent owns the raw value and debounces it) with no submit button. LoadMoreButton renders "Load more", "Loading more…", or "That's every public repository." purely from the hasNextPage/isFetchingNextPage flags it's given. RepoCard's title links to the internal detail route and shows fork/archived badges, topic pills, and the extended stats row described above; a separate "View on GitHub ↗" link goes to the real repo.

Notes

  • No tests for the React components themselves, per this project's testing standard: every service/utility function gets tested, UI-only components are exempt unless there's real branching logic worth covering. useDebouncedValue follows the same precedent already set by useGithubRepos/useRepoDetail: it's a thin React-timing wrapper, not independently tested.
  • A two-segment URL that isn't a real repo (e.g. /nonsense/nonsense) shows a real "Repository not found" error from RepoDetailPage rather than the generic 404 page — this is intentional: the URL shape genuinely matches owner/repo, so attempting the real lookup and showing GitHub's actual answer is more informative than a blanket "page not found."
  • Switching to a username that's never been searched before no longer shows a loading flash: placeholderData: keepPreviousData (src/hooks/useGithubRepos.js) keeps the previous username's results on screen, dimmed via the .repo-list-stale class, until the new username's data arrives.

About

GitHub dashboard with cached search, background refetch, and pagination via TanStack Query.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors