Never been to vose.tv before? It's a web app which pulls YouTube videos off Reddit and feeds them to you in a nice watchable format. It's my spiritual successor to reddit.tv.
I created vose.tv three years ago as one of my first React.js projects. I knew how to use React.js and Redux already, but still lacked experience making fuller applications. Now with several React.js projects under my belt, I decided to apply some of the new things I learned. Here's a summary of what that was.
Every refactor needs a new feature, I wanted to add Reddit's alternate sorting methods. The application architecture wasn't well suited for adding these, and a business logic refactor was needed.
Most of the time the default sorting—hot—is fine. These alternative sorting methods like new and top are great ways to browse less popular subreddits.
About a year ago Reddit changed it's algorithm for displaying upvote counts to better reflect actual counts. Reddit formats these as 1.0K instead of 1000, it only made sense to do the same.
While popular subreddits requests are cached by the server and refreshed every 5 minutes, others need to be crawled by the server for each request. This can sometimes mean a page load of 0.5–1 s. To improve user perception I changed the loading screen into skeleton components.
Improvements like 0.5 opacity on current content, then after 200ms, show preview components.
Updating the build process
When I first made vose.tv, browserify was still my bundler of choice, and I had still not moved on to webpack. So the first thing I did was copy over some more modern scripts I use in other projects.
This wasn't totally necessary, but it did bring in hot module replacement which really helps make testing during development quicker.
Later on in this refactor, Parcel--a module bundler--came onto the scene. If you don't know about Parcel, it's the config-less bundler. My build scripts I normally use weren't working perfectly, and I didn't feel like debugging them, so I decided to replace them with Parcel. It took me about 30min, I cut over 500 lines of code, and many dependencies. Most of the time was spent deleting code, and trying to get Babel 7 to work (it didn't). By the time I finished this refactor, Babel 7 support was added and I was able to upgrade.
Dropping support for legacy browsers
After reviewing analytics data, I saw that none of vose.tv's users used legacy browsers, such as IE11. This allowed me to use newer CSS features, and drop a pollyfill for scrollIntoViewIfNeeded.
Reducing bundle size
I really wanted to get the initial download down from what it was, to do this I had to make some sacrifices.
The first thing to go was webfonts. I was using Droid Sans and some of its variants. While the thin font did look nice for the larger text, slight design tweaks were all I needed to make system fonts look good. This not only saved on the site weight, but also alleviated having to use any font loading strategies.
My next step was to reduce the logo size. The logo uses a version of the Futura typeface, converted to SVG. My plan to reduce the size was to simply align all the paths on a pixel grid, rather than on sub-pixels. I admittedly spent a lot of time tweaking the path anchors for what I thought would be a bigger improvement. Though it was still an improvement, and maybe I can go further another time.
Vose.tv was still on version 0.14 of React, simply updating it was enough to save some bytes.
Bundle Visualiser lets you inspect which modules are included in your bundle, and their size. I noticed that
@reach/menu-button actually included all of
@reach/router which I didn't use. Another saving was with
youtube-player, which includes
debug as a dependency. I forked these and removed the dependencies myself. Without the bundle visualiser it would've been very hard for me to find these useless dependencies.
Lazy loading thumbnails
The new Intersection Observer API allows you to detect if an element is is in the document's viewport when scrolling. Using
react-intersection-observer I made video thumbnails lazy load. It was a pleasure to use, and I hope to use it more in the future.
I absolutely love Redux, especially now that I've learned to use it well. When I first wrote vose.tv I didn't fully understand the best way to write reducers and actions, so supporting the new feature I wanted would require a rewrite. Instead I thought the state was simple enough I would try to remove Redux completely. I'm not sure this had a net gain, and I wasted more time than any other part doing this.
Adding and removing Unstated
Unstated looked very cool and I decided to give it a shot. It worked well, albeit the slightly confusing documentation. It was advertised by some big names in the community, and I fell for the hype.
I realized that it didn't offer much over what it was trying to replace, React's new createContext. Funnily enough, it includes a polyfill for createContext as it's built on that API. I'm not sure why the polyfill is needed. Anyone building an app with a new state library should probably be able to use the latest version of React, or be able to include a polyfill themselves.
Unstated doesn't have an obvious path for server-side rendering like Redux does, in part due to how injecting data is more complicated than it needed to be.
Overall it offered nothing interesting over using createContext directly, so I decided to use that directly instead.
Current state solution
Because vose.tv's component tree is fairly flat, not much prop drilling is needed. I decided instead to pass down props directly from a container component and avoid using Context all together.
Like Redux, my state solution needs to be preloaded with server rendered data. This data is just JSON included in the html when the server renders the app.
I analyzed this for potential savings. I found a few unused properties, some text that I could dedupe (like youtube id and youtube thumbnail), and shortening Reddit links. By also adding default props to components, I could then conditionally include props instead of always including them in the state.
CSS files next to component file
One thing that made my life much easier is keeping CSS files named the same as their components. This time I took it one step further and simply put them in the same folder as their component. The reason I didn't do this before is because importing them was more difficult, but with parcel all I have to do is
import './style.css'; in the respective component to include it's styles. You can set this up in other bundlers as well, but I never took the time to do it before. With parcel I had to do zero work.
CSS Grid Layout
The layout for vose.tv was built using absolute and fixed positioning. I felt like CSS grid layouts was a much better approach for this layout, and without having to support legacy browsers it was a no brainer to update. The transition was fairly straight forward, and initially the code became quite simple. After working on the mobile layout, the code grew a bit, though it remained much simpler, and easier to comprehend. This also allowed me to refine the mobile layout a bit more, which wasn't too great before.
I wanted to unify the colors and try to simplify building new components. Using CSS variables makes this a dream. SASS and other preprocessors always made this possible, though when developing a design it's never easy to tell how best to organize these variables. The first refactor is a great time to do a good job.
A benefit of CSS variables over preprocessor variables is they are truly dynamic. This gave me the option to easily swap themes. For fun I made a light theme that took about 5 minutes. There's no option for it yet, but maybe in the next round of updates I'll add it.
Once I had all the colors in one place, I also made sure the color contrasts were WCAG compliant.
Since I was adding more dropdowns for the sorting, I had to make my dropdown component a little more reusable. Initially I made it using React's new portals, which allowed me to simplify the CSS, but I still had to implement clicking out, keyboard support, and focus management. This was way too much and I was going to just do the first point. I then saw Ryan Florence's work on Reach UI. Reach UI is a collection of accessibility components with built in focus management and other accessibility features. As you'd expect from someone that does a lot of teaching, and put a ton of thought into writing React.js components, these are pure bliss. It barely took me 10 minutes to replace my old dropdowns and gained reusability, and full accessibility support.
With Concurrent React on the horizon, I've been seeing ways my state structure could be improved and over all experience be better using these new tools. I can't wait to get my hands on it.