/ #ffffff

QuickHub for GitHub

I've just joined Commit as an Engineering Partner!

As part of my onboarding, they gave me free rein to "scratch an itch" as it was put. The catch? I had to present it back to an open invite Zoom chat, no big deal!

After toying with some ideas I settled on a Chrome Extension. I've built a couple of simple "new tab" pages before but QuickHub would require a fully functioning popup and integration with the Chrome API's, this was all new to me.


The Idea

I usually have a GitHub tab or two open and often find like many other apps they get buried in an overloaded tab bar.

Seen worse.

The plan was to surface links to the most common GitHub items:

  • Personal repositories
  • Organization repositories
  • Notifications

and built with:

  • React
  • GraphQL
  • Chrome

Getting Started

The first thing I did was reach for Next.js but found it wouldn't be suitable and then went for create-react-app to get a front end running asap.
All of this would later turn out to be a waste of time.

The second thing I did was remember I don't know anything about the Chrome Extension platform.
This should have been first.

Even still, brimming with misplaced confidence I skimmed the docs, skimmed a few articles and then started on the GitHub auth docs where I quickly found that you have to authenticate server-side. Well, I don't have anything server-side backing this, nor did I plan or want to build a server. Immediately this small project was escalating.


Authentication

Firebase to the rescue! (mostly)

At this point, I learned that following someone on YouTube who seemingly knows the Chrome platform, doesn't mean they actually do.

The Firebase UI kit isn't supported in a Chrome Extension - even though you can make it work, as my YouTube friend demoed. What I found was that when you close an extension popup, any kind of state is emptied so the app would require the user to log in every time they open it.
To persist the auth state Mr. YouTube was sending a message to the extension background.js through chrome.runtime which can persist state and setting an auth variable to true. Which is utterly ridiculous if you think about it. There are a bunch of ways in which the user could become logged out but this app would blindly say "all good bro, I've got a true over here!" 🤦🏻‍♂️

So I removed Firebase UI, read the Firebase docs, implemented the auth flow on the extensions background.js and built a frontend to communicate with the background.
This way Firebase is able to persist the user state and when a user launches the popup I check the real auth state with firebase.auth().currentUser and allow them to continue knowing I have the correct access token or redirect them to log in again.

From this endeavour, I did at least learn more about chrome.runtime which is the crux of the chrome extension framework


GitHub

The GitHub API is extensive.

While this app only requires read access, unfortunately, to get private repositories I have to request full access, which seems unnecessary? While setting the access scope is easy I did get tripped up by this API.

When you've logged in the GitHub API returns the user profile with a bunch of API paths to follow to obtain more user data.
For example to get all the user repos: https://api.github.com/users/searleb/repos
How good is that?!

Well, it turns out not very. This endpoint returns public data, even if you're authenticated with private access. It took a while to figure this out and the solution I came up with was to use the search endpoint and construct the URL myself which looks like this:
/search/repositories?q=user:searleb which does return private info if authenticated.
This doesn't seem intuitive so I still wonder if I missed something in the docs?

Once I'd got over that and built out all the API calls needed I found my background.js was getting big. It contained all the Firebase and GitHub logic and as well all the event listeners communicating with the popup.


Front End

Communication between the popup frontend and the background is done via chrome.runtime.sendMessage and chrome.runtime.onMessage.addListener methods

I decided to build out a flow that implemented an "optimistic UI" of sorts using React Context and caching with Chrome storage.

  1. Front end sends out a message to the background requesting data.
  2. Background listens for the message and returns local storage data and calls the GitHub API.
  3. Front end updates context state with existing local storage data.
  4. Fetch response is saved to chrome.runtime.local.storage.
  5. Background listens for changes to local storage and sends out a message with updates.
  6. Front end listens for storage changes and updates the context state
Data flow diagram.

This system does exactly what I set out to do but it doesn't scale well. I started to feel even with this small app it became a bit monolithic in a way. Everything was tied to a single context that contained all app data. Fetching data for one component meant making edits to three or four files. Storage changes are broadcast even if the front end doesn't need it and if multiple messages are sent in succession I ran into a race condition where the React state would get out of sync. This was fixed by combining the data into a single message on the background but it was still a gotcha to work around.


Starting to rethink things

By now I'm starting to get a better grasp of the chrome extension framework but because I was using create-react-app I had no control over the build so background.js was left hanging around outside of the build process.

Goodbye CRA!

I removed everything and built my own pipeline with Webpack. Now I was able to split out the background event listeners, Firebase and GitHub logic into their own files and import them. 🧹

This also gave me control over the build output. Extensions have some particular requirements, like the naming of some assets and a manifest.json that I could now automate in the build process.

Being able to add in a Tailwind.css build set was a bonus too.

I also started to move only really "global" data into context to share between multiple components and component level data into local state. This does simplify the data flow and keeps state close to where it's used but it means I no longer have the optimistic UI.


MVP

At this point, I had a fully functioning app that does everything I set out to do and so it was demo time.

The demo itself only took a few minutes but when you have a room full of engineers there is plenty to talk about even with a single page app that just gives you a bunch of links! So another 45 mins went by where we all chatted about the tech, approach, decisions and challenges I had putting it all together.


Next

I would like to improve and simplify the data flow and bring back data caching for that fast UI.

I never got to build with GraphQL, I went with REST because I'm more familiar with it which meant I could get moving quicker in the limited time I had. The REST API returns tons of info and I'm using almost none of it so moving to GraphQL makes a lot of sense.

This app does work in Firefox as well but I discovered that Firebase doesn't support Firefox which is a bummer.


Summary

I learned a lot building this project, aside from getting pretty conformable with the Chrome extension framework and some of the GitHub API, I learned more about actually learning.

I've been building websites since 2013 and I think I had become so used to Next.js and the frameworks I'm familiar with that when I jumped into this, building on a platform that is web-based but not exactly how we're used to in the browser, I didn't take the time to go back to basics and get to know this new environment. I really didn't need to reach for any js library to get started. At the start, this was a hindrance as it got in the way of learning new things.

Trust myself and don't try to cut corners. YouTube, Medium, StackOverflow etc all offer loads of helpful insights but they're unlikely to cover your exact use case and "best practices" are a moving target.
That YouTube video I watched promised to walk me through the Firebase integration with a platform I was unfamiliar with. It took an approach I wasn't considering initially but I figured this would just get it done quickly. While I did learn some stuff I also had to undo the entire thing and go back to doing it the way I had imagined in the first place.

Overall I'm happy with it. The code is open source, PR's and suggestions are welcome!


QuickHub GitHub repo Download QuickHub from the Chrome Store