In part one, we created a sidebar that lets us comment on articles. We built a
React component that auto-aligns itself with specific words or phrases. The
<Aside>
component binds a listener in case the window is resized:
In this part, we’ll extend the component with additional functionality to make it more robust and better-able to cope with slow-loading content, print layouts, etc.
Currently the component positions itself when mounted, but what happens if images and fonts take a few seconds to load asynchronously? They might affect the layout by pushing content down, throwing our comments out of alignment. Something like this:
What we really want is for comments to re-align themselves after images and other slow-loading content has loaded. How can we get this to work?
We could try and listen in to these events but that would mean polluting our app
with event listeners for all manner of things: we’d have to register onLoad
events for images and try to use the
Font Loading API to
detect when fonts have loaded.
The simplest approach I’ve found is to just use timeouts re-align comments at set times in anticipation of slow-loading content. This is a hack and isn’t perfectly reliable, but I’ve found it works well enough in practice and it’s easy to implement:
There are cases where this approach won’t work. For example, if the user can
expand something on the page, the layout might change. We could additionally use setInterval
and periodically
re-align elements every few seconds as well.
To avoid cluttering our component, we can extract functionality into React hooks:
While we’re at it, let’s do the same for our resize listener:
This neatens up our component considerably:
We’ll use this approach of extracting hooks to keep our component clean as we add sophistication. It also means these behaviours are modular and can be reused later.
Occasionally, when resizing the window, React would throw the following error:
I think this happens because of a race condition between the resize event and
React’s event loop. Sometimes target
refers to an undefined
element.
This shouldn’t be the case because the
referenced element always exists and is before the <Aside>
in the DOM, but
React might be in the middle of re-rendering the component and hasn’t updated
its ref
yet. It throws an error when we try to use it.
We can fix this problem with a guard condition in the align
function:
The above error draws attention to the fact that the browser fires hundreds of events when the window is resized. This exasperates the problem because React competes with the resize handler for execution time, making the race condition more likely.
We’ve fixed the problem by guarding against it, but it’s probably sensible to also reduce the number of re-alignments per second. There’s no need to re-align hundreds of times in a row as the user drags the edges of their browser to resize it.
In the same spirit as before, we’ll use a hook for this:
Now when the user resizes their browser, there will be a delay of 50 milliseconds before comments are re-aligned, vastly reducing the overhead of resizing.
This is noticeable, too. Before, when resizing the browser, the page was glitchy and slow to update. It’s much smoother with debouncing. I didn’t write the debouncing implementation, I used this one from Tom Stuart and Paul Mucur.
Honestly, printing web pages is a mess. You’d expect to get more-or-less the same version of the page you can see when you hit print, but that’s not how it works. Instead, it’s steeped in history and works in arbitrary and unexpected ways.
For example, some browsers fire onbeforeprint
and
onafterprint
events. There’s also
MediaQueryList
but only
some browsers
support event listeners. Other browsers scale content down by default and
remove backgrounds and images.
I don’t actually think anyone will print these pages. The reason I care is because printing has subsumed a feature people do actually use: exporting web pages to PDF.
I’d like to be able to offer downloadable versions of articles for offline reading or perhaps even produce a short ebook someday. It’d be nice to use all the features I’ve developed for this blog without having to familiarise myself with a new tool.
When I first tried to print, I found the alignment of comments in the sidebar was slightly off. Everything was a bit lower than it should be and this was exaggerated further down with comments toward the end of the article. What’s going on?
I think what’s happening is a fresh version of the page is rendered when the print dialogue is opened. It changes the layout of the page by scaling its content and setting margins but doesn’t trigger a resize event to re-align the comments.
You can control some things through print stylesheets and @media print
style
rules but it’s harder to reliably detect in JavaScript when the page is being
printed.
Eventually, I wrote another hook that works most of the time:
I then use this hook in the <Aside>
component to trigger a re-alignment. It’s
important not to debounce these calls. If we do, the print dialogue blocks the
page while it renders a print version and we’ve missed our slot.
Here’s how use the hook:
This hook isn’t perfect but it seems to work well in Chrome and Safari. I still need to do more work to add a print stylesheet to make it look nice but at least it’s no longer broken... except in Firefox which likes to overlap all the comments:
I’ll try to fix it someday but for now I’m happy it works in WebKit. That means I can produce prepared PDFs, rather than encourage anyone to use the print dialogue.
It can look messy if <Aside>
elements jump around a lot as the page loads.
This will happen a bit as slow-loading content is added to the page but we can
smooth things over a little by fading in our comments after the first alignment
has happened:
const align() { //... setStyle({ top: offset, opacity: 1, transition: "opacity 0.3s" });}
Also, if you’re using <Aside>
elements a lot, be mindful that each time you
do, even listeners are being registered on window
. This can add up over time:
It would be better to register a single handler for each type of event and re-align all comments through that. I haven’t implemented this but will probably add this later.
In this article, we’ve taken a detailed look at how to build a commentary sidebar in React. This has a few rough edges but overall I’m really happy with how it turned out.
It’s already become an integral part of how I think about writing. A tool I can reach for when I want to say a little bit more about something, without being too distracting.
I’m really enjoying the process of writing and learning React so it’s been fun to combine the two. For reference, here’s the most up to date version of the code: