Jake Donham > Technical Difficulties > Read the Code: GitHub Flat Viewer

Read the Code: GitHub Flat Viewer

2022-01-26

One way I like to learn new a computer thing—a new library, language, tool, or technique—is to read the code of projects that use it. I don't do this enough! I usually start by following a "getting started" tutorial, skimming the documentation, and looking at example code. But often the distance between getting started with a computer thing and using it effectively is very large. So reading the code of a substantial project can be really helpful: it's working code with a purpose; you can see how the new thing works and how it fits together with other things. I also like reading project code because I usually learn a lot of things beyond what I set out to learn, like tools and libraries I hadn't heard of, or programming styles and idioms.

I'm starting a new job soon at GitHub Next, so I thought I would read the code of some existing GitHub Next projects to get a sense of how the team works. I started with Flat Viewer, a browser app that reads flat data files from GitHub via the REST API and displays them in a table interface with sorting and filtering. I think it's a pretty straightforward browser app, but I am not a very experienced browser app programmer, so I learned a lot. In what follows I'm going to focus on the parts that are new to me, so this will be a bit of a hodgepodge, but I hope there's something in it that's new and/or useful for you.

The world of libraries, frameworks, and tools for building browser apps is immense and ever-changing—"best practice" is a destination forever receding into the distance as you approach—so please read this as a description of one way of doing things, that seems to work for a particular purpose, as one person understands it, rather than anything like an authoritative explanation.

Flat Viewer is a React app written in TypeScript. Good, I feel pretty comfortable with this! Let's walk through the components:

App

The main component is App, and the main thing it does is route to different views within the app based on the URL path, using React Router. In a single-page React app, navigating to a view doesn't interact with the browser's URL bar: history is not recorded, and you can't get back to a view by copying and pasting its URL. React Router parses the URL and routes to a matching view; navigating to a view updates the URL (and records it in history). This works using a pattern-matching syntax inside of React JSX tags, like so:

<Router>
  <Switch>
    <Route exact path="/" component={Home} />
    <Route exact path="/:org/" component={OrgListing} />
    <Route path="/:owner/:name" component={RepoDetail} />
  </Switch>
</Router>

An empty path / routes to the Home component; a one-segment path like /githubocto/ routes to the OrgListing component; a two-segment path like /githubocto/flat-demo-bitcoin-price routes to the RepoDetail component.

There are a few other things going on in App:

Home and RepoForm

The Home component just adds some styling around RepoForm, which shows a form for filling in a GitHub account and repo. There are several interesting things going on in RepoForm:

There's always a tradeoff between calling out to a dependency and writing the code by hand—I would probably write something simple like this by hand (keep track of field values with React.useState and validate them inline). But I can see that for a big form with intricate validation, using Formik and yup would be much easier; and if they are in your toolbox already it's easier to use them even for simple things.

OrgListing

The OrgListing component displays a list of repos from a GitHub account. The list of repos is queried from the GitHub REST API using wretch and React Query.

Wretch is a wrapper around the browser built-in fetch function (which makes an HTTP request and returns a Promise of the result). It replaces fetch's "big bag of options"-style interface with a "builder"-style interface, and provides some prebuilt handlers for HTTP-level error responses and for decoding response bodies. Here's the wretch call to get a list of repos (see fetchOrgRepos):

githubWretch
  .url(`/search/repositories`)
  .query({ q: `topic:flat-data org:${orgName}`, per_page: 100 })
  .get()
  .json();

It calls the API (at https://api.github.com; this is pre-configured in githubWretch) with the given path and query params (see the API docs), then decodes a successful response as JSON. (Note that the search matches only repos tagged flat-data.)

React rendering is basically synchronous, but wretch returns an asynchronous Promise. This is where React Query comes in: it keeps track of the state of a query—succeeded (with some value), failed (with some error), or waiting—and rerenders components that depend on the query when the state changes (e.g. the Promise resolves). To query the list of repos, the wretch query above is wrapped in a useQuery hook (in useOrgFlatRepos):

useQuery(
  ["org", orgName],
  () => fetchOrgRepos(orgName),
  { retry: false, refetchOnWindowFocus: false }
)

The return value of useQuery includes a status field of type "success" | "error" | "loading"; depending on status it has additional fields describing the returned value or error. OrgListing renders an appropriate component (RepoListing, ErrorState, or Spinner) depending on status. RepoListing renders a list of repos, each with a React Router Link to navigate to the RepoDetail view.

The arguments to useQuery are a label (used for caching and deduplicating queries), a Promise-returning function that makes the underlying query (you can use any library you like), and some options controlling when the query should be retried or refetched (by default, focusing away then returning to the browser refetches the query).

RepoDetail

The RepoDetail component displays controls to select a data file and version from a repo, and a table view of the selected data. By default the first file (having an appropriate format) in the repo and latest version are shown.

The state representing the selected data file and version is tracked with use-query-params. This provides a useQueryParam hook that works like React.useState:

const [filename, setFilename] = useQueryParam("filename", StringParam);
const [selectedSha, setSelectedSha] = useQueryParam("sha", StringParam);

but serializes the state value (StringParam specifies how to serialize it) and stores it in URL query params. It integrates with React Router; updating a state variable (e.g. calling setFilename) causes a navigation, so the state is restored if you copy/paste the URL or press the back button. Nice! There is a lot I don't understand about how React Router and use-query-params work under the hood—it would be worth reading this code, but not today.

When RepoDetail renders, it first queries a list of files in the repo (with useGetFiles and fetchFilesFromRepo; see the API docs) and sets filename to the first valid filename. Next it queries the list of commits for the file (with useCommits and fetchCommits; see the API docs) and sets selectedSha to the hash of the latest commit. React Query has a nice way to chain asynchronous queries: useQuery supports an enabled flag, which the useCommits query sets only when the useGetFiles query has completed successfully.

UseCommits / fetchCommits refer to the type of the API response using Endpoints from @octokit/types:

type listCommitsResponse =
  Endpoints["GET /repos/{owner}/{repo}/commits"]["response"];

This is very cool! Apparently the types are scraped from the API docs. It looks like Octokit doesn't have full types for the search API (used in OrgListing), so those types are written out explicitly. (I wonder why Flat Viewer doesn't use the Octokit client.)

Commits of data files created by Flat Action contain the URL of the data source (see example); RepoDetail parses it out (with parseFlatCommitMessage) in order to display a link.

Finally, RepoDetail renders its controls (with components to show file and version pickers) and the data file itself (using JSONDetail). There's also a "toast" component for displaying error messages, and a "disclosure" component that makes the controls hideable on smaller displays; I didn't dig into the details.

JSONDetail

The JSONDetail component queries the contents of a data file (with useDataFile and fetchDataFile) and displays the contents using flat-ui. It also displays a picker for keys within the file—I guess it handles JSON files with a top-level object containing multiple arrays? FetchDataFile parses the file as CSV, TSV, YAML, or JSON based on the file extension (using d3-dsv and yaml), and has some special handling for GeoJSON / TopoJSON files.

Flat-ui has controls for filtering and sorting data, but it doesn't maintain its own control state; JSONDetail passes props for the control state and an onChange callback, and uses use-query-params to track the state in the URL:

const [query, setQuery] = useQueryParams({
  tab: StringParam,
  stickyColumnName: StringParam,
  sort: StringParam,
  filters: StringParam,
});

You pass useQueryParams an object of serializers (the keys are used as param names), and you can pass a partial object to the setter if you want to set only some fields. (There's a nice use of type-level programming to give query / setQuery the appropriate types based on the serializers.)

There is a lot of cool stuff going on in flat-ui and I would like to read that code some time.

Styling with Tailwind

All these components are styled with Tailwind CSS, so there are lots of class names sprinkled all over the place. I found this very mysterious! Here's what the Tailwind docs have to say about it:

Now I know what you’re thinking, “this is an atrocity, what a horrible mess!” and you’re right, it’s kind of ugly. In fact it’s just about impossible to think this is a good idea the first time you see it [...]

I really appreciate honest documentation!

As far as I understand the pitch: the traditional way to do CSS is to write "semantic" classes that describe the style for a particular component in the app, then use the class name whenever that component appears. The Tailwind way is to use "utility" classes that describe particular CSS attributes, and string together the ones you need when you need them. This might cost some duplication (similar components might use a similar set of utility classes), but saves the pain of writing and maintaining a hairy CSS file.

I've used styled-components, which is a different way to avoid writing and maintaining a hairy CSS file, so this makes sense to me; but it's a whole other language to learn. It would be cool if you could hover a Tailwind class in VS Code and see what it actually means... oh look.

That's it!

Please email me with comments, criticisms, or corrections.