Creating a Chrome Extension seems to be way easier than expected.
Just create the app:
npx create-react-app chrome-another-tab --typescript
Add the Chrome API TypeScript types:
cd chrome-another-tab && yarn add @types/chrome
And update public/manifest.json
like you'll do for a standard Chrome extension.
Make sure to set the correct Content Security Policy (see below).
P.S.: Install and use Prettier 👙
Latest versions of Chrome block inline scripts in Chrome extensions. Since Create React App 2 creates an inline script to run the app you might stumble upon an error message which is related to Content Security Policy (CSP) in the console.
In the error message you will get a SHA value which can be added to the manifest.json
file to solve the problem.
The Chrome APIs use a callback-last convention so I'll promisify and wrap them in chromeService
.
The development mode build of Create React App (created by webpack-dev-server
) seems to be stored directly in memory.
This means that to test our extension we have to always create a production build.
As a workaround I'll mock a the result of the Chrome API with a few fixtures while in development.
See chromeService
.
The Chrome Bookmark API doesn't return the bookmark favicon.
I investigated a bit and it looks like the two most common ways to grab it right now are:
- Use the
chrome://favicon/
API (e.g.:chrome://favicon/twitter.com
) to get the favicon icon, or... - Use the
https://www.google.com/s2/favicons?domain=foobar.com
Google service.
Well, guess what, both services return the 16x16 favicon, which looks crappy nowadays.
Thanks to a StackOverflow comment though I was able to discover faviconkit.com which looks amazing: you can fetch up to 144x144px icons and it uses an aggressive caching strategy (both in the browser and on the CDN) so it's incredibly fast.
The website says that the project is in "free beta" but given how well it performs I suspect it wont' stay free for long.
I still don't know why the Bookmark API doesn't return the 32x32 favicon used in the Chrome bookmarks management (I tried taking a quick look at the Chromium source but I wasn't able to find anything related to that).
I really like the the shape of the bookmark tree returned by the Chrome API.
Since it's a tree it might look scary at first but once you get the pattern it just "click", and you can easily iterate trough it using recursion (see ./src/utils/parseBookmarkTree.ts
as an example).
Shoutout to the @types/chrome
type-definitions: they are immensively helpful for exploring the Chrome APIs.
Not much to say here, I added styled-components.
Why styled-components instead of any other library? Well, it's an easy choice for me: it's one of the most used CSS in JS libraries and I like its API (especially in the latest versions).
Since I'm planning to add a few features that need to share a global state between different components I had (wanted) to pick a state management library.
Initially I thought about just using the Context
API, maybe in conjunction with the useReducer
hook, but since in my last project I fell in love (again) with Redux + TypeScript (and a few other related libraries) I decided to use them here as well.
The libraries I'm planning to use are:
- Redux
- Redux-Saga
- Typesafe-Actions
- Redux-React-Hook
- Immer
Yes, I know: so many libraries to handle the state when I could have go for MobX ot other smaller libraries...
Well, there are a few reason why I really dig this setup:
- Top-notch TypeScript integration. Everything is type-checked, auto-importable and I feel like I can trust TypeScript to be able to catch even the more subtle error. The only margin of improvement that I can see is a better typing support for Redux-Saga.
- Small boilerplate. The boilerplate is way smaller than what you'd expect from a standard Redux setup: Typesafe-Actions allows you to define the actions in a simple way, while Immer takes care of the reducer complexities. And also...
- Hooks. Redux-React-Hook and a small custom hook makes the connection between Redux and React components simple, clear and strongly typed.
The main drawback here (which might be a deal-braker for someone) is that you'll need to learn the API of 5 different libraries...
Added an option to show/hide bookmarks.
Finding a way to handle the show/hide flow has been really interesting.
My initial idea was adding an "Edit mode" that, once activated, allowed the user to hide/show the bookmarks and in the future rename/move them.
After creating a stub of it and playing around though it seemed a bit too clunky for my use cases so I changed the pattern: I'm showing the "hide" options as a floating button that appears on a bookmark after hovering it.
This also allowed me to experiment a bit with the hovering micro-interaction... since the option was showing up every time you hovered on a bookmark (which is a common action) I added a small delay before showing it, so it won't show up while you're moving between the bookmarks.
The "hide/show" option icon is small enough to allow me to add another option in the future (if needed).
I also took the chance to add a custom made persistence/rehydration logic (redux-persist seemed overkill, so I'm just using two small sagas for that) and a button to show/hide the hidden bookmarks.
New look! This time I went for a stronger color palette and I reorganized the header.
Honestly, I haven't put too much attention in details on the UI yet because I'm pretty sure I'll change the theme a few times during the development. Ultimately I'd like providing a simple way to switch between different themes or even to provide your own color scheme...
Added a search feature.
I can now search through bookmarks using the top searchbar.
The bookmarks filtering is currently done with a Redux selector that takes into account the bookmarks visibility and the current search query.
I'd like to add a search shortcut and a way to specify if the query should be searched even on hidden bookmarks (right now you have to press the "Show hidden" button to do that).
Well, it looks like I was wrong.
The chrome://favicon/
API allows you to specify the size of the favicon you need (e.g.: chrome://favicon/size/16@2x/${url}
), see (./src/utils/getFaviconUrl.ts)[getFaviconUrl.ts]: I'm now getting the favicon from the chrome API when the app is running as an extension and I'm using FaviconKit API to get it when running/testing/developing locally.
While working on a keyboard handling hook I discovered that when CMD key is kept pressed, keyup is not triggered for any other key
.
Looks like it is a known behavior of the meta key without any available workaround.
That's not blocking in any way, but it's interesting nevertheless.
Got some free time today so I took the chacne to publish it on the store. Publishing a Chrome Extension is quite easy, and seeing that the update is "immediate" is absolutely refreshing if you come from a native app development background.
You can find the extension here.
Following this really good article I was able to setup a CircleCI Continous Integration/Deployment of the extension in 20 minutes.
The CI runs a build
, tsc
and tslint
command after a push, and if the target branch is master
it also zips and publish the new extension version to the Chrome Web Store.
I already have a few ideas on how to improve the process but it is more then enough for now.
This weekend in the free time I worked on implementing a feature that allows sorting the bookmarks trough drag & drop.
In the past I already worked on custom drag & drop solutions using react-dnd
and I also played around with the awesome [react-beautiful-dnd
](https://github.com/atlassian/react-beautiful-dnd).
This time though I'm dealing with grids instead of list, so react-dnd
would have been a bit too complex to use (at least for my weekend free time) and react-beautiful-dnd
doesn't work with grids.
So, I investigated a bit and have noticed that react-sortable-hoc
seemed to suit well my use case.
Just like its name suggest, react-sortable-hoc
is a set of Higher Order Components that makes your components "sortable". It works with both list and grids.
The library didn't work out of the box for me though, because:
-
CSS grids are not supported yet. I noticed that there was an open PR that add CSS grids support and it was working fine for me, so I updated it and sent-it again. Since it has not been merged yet, I had to deal by myself with it in the code by "simulating" the CSS grid gap property using margings, at least until the PR is merged.
-
react-sortable-hoc
requires the "sortable" list item component to be the direct child of the "sortable" list/grid, so I had to make a few changes to the components structure to make them work. -
react-sortable-hoc
adds some CSS on top of the "sortable" items that might requires some adjustments to your own CSS. In my specific case for example I had to turn off my owntransition
while the item is dragging the item and turn it on after the drop.
That said, the library works well! (Even if I admit I'm a bit worried that will stop be officially maintained soon).
I also had a lot of fun playing around with the Chrome API for moving bookmarks, discovering what seems to be a really old bug of it, and implementing an optimistic update (so that we can update the bookmark position in the state without having to wait for the Chrome API response).
P.S.: Looks like "Bookmarks Bar" and "Other Bookmarks" cannot be sorted :(