We recently released a significant update to the way users filter and explore their error data. I want to discuss how we built it, the tools we used, and how we’re doing things “differently” than everyone else.
The Existing UI
The New Filter Bar
As we thought about ways improve our filtering capabilties, it became clear the new UI would need to be more interactive to handle the features we wanted. More features meant more interface complexity. The final product allows the user to add and remove filters, search for new filters with autocomplete, and add recently viewed items. This is what it looks like today:
With the additional interface complexity, our usual approach for handling client-side interactivity (hand-rolled, mostly-vanilla JS) would be stretched thin. We’ve long resisted heavy client-rendered frameworks, but perhaps it was time to try one?
No One Ever Got Fired For Choosing React
We come from varied development backgrounds, and between us we’ve used almost every major JS framework. We all agreed that React had the right mix of benefits for what we were trying to accomplish. Since we use server rendering, we wanted a framework that could be used on only a small portion of the page. We also wanted decent community support and reasonable performance (read: no Angular!). We don’t like the build/transpiling step required with React, but what can you do?
Now With Types!
Our back-end is written in C# and we’re fans of strongly typed languages. If we’re going to have a transpiling step anyways, why not try TypeScript? When React and JSX first gained popularity, TypeScript support was hit or miss, but these days it’s a first class citizen and setting it up was not too painful. (Note: While much cheese was moved between major versions of Webpack, version 4 does make things much easier out of the box)
Here’s what our webpack config looks like. This is our entire dev config. Most of this was cribbed from the official TypeScript docs.
Our complete dev webpack config. Things have improved markedly since the Webpack 1 days.
Server Rendering And React
Choosing React and TypeScript in 2019 are not terribly contrary decisions. And didn’t I say something about “being different”? Indeed. We were unwilling to change our current server rendering strategy (PJAX, Razor, ASP.NET MVC) since it works so well. And we were not about to shoehorn Node in to the pipeline to server render the React components separately. Going full single-page-application was a nonstarter as well - we weren’t going to convert our Razor templates to React (if I never use React Router again it will be too soon). So how do we server render the bulk of the page, but still render our React components client-side (and do it seamlessly)?
Every time we do a PJAX page load, we first unmount the top-level
GlobalFilter component using
window.unmountGlobalFilter(). Then we load the new HTML from the server and replace it in the DOM. Finally we can render our
GlobalFilter component again using
window.renderGlobalFilter(globalFilterData). (For those curious,
globalFilterData is a large JSON blob we bootstrap each page with from the server; it contains the current state of all filters in the user’s session.)
Once the top-level component is rendered, users can interact with it. Naturally, the top-level component has many child components. Those child components need a consistent shared view of the current state. There’s a host of options, and we didn’t like any of them.
The Usual Suspects
The most obvious choice for state management is probably Redux. But I think it’s too much ceremony for too little advantage. MobX is worse. Observables?! The whole point is one way data binding and flow. Plus, observables are big footguns that will cost you major amounts of time and money in the long run.
From the React team themselves, there’s the newly blessed context API. I don’t like this API because it’s clunky and awkward. It’s also not really meant for our specific use-case.
I’d be remiss if I did not also mention hooks. There are some advantages for specific use cases, but again, sharing application level global state across child components didn’t feel right. Plus it looks like a leaky abstraction to me (see: ESLint rules to make sure you don’t use it wrong). Time will tell if it’s actually worth using for anything.
Doing the Thing You Always Tell People Not To Do
So we did what any novice developer would do: we built our own state management solution. Yeah, yeah, I hear the objections already, and they are well founded. But we did it anyways and it worked brilliantly.
The concept of a “Store” seems consistent throughout most of the state managers in React-land today. That is, you have a single place where the shared mutable state resides. When the state changes, a re-render is triggered. In our case there is a list of applied filters that is shared across all components. So we made a
FilterStore knows about the current state, and can modify it if necessary (and modification will trigger a re-render). It exposes an external API that abstracts away common actions for consumers.
Our very simple
To wire it up we call
FilterStore.initialize() in the top-level component. This ensures that any call to
setState() inside the store will trigger React’s re-render logic.
Inside GlobalFilter.tsx, our top-level component
Now any child component can get an up-to-date look at the currently applied filters. Or make changes as necessary.
Using the FilterStore from any child component
In some ways it’s crude, but there is minimal indirection and complexity. So far it’s been a reliable solution for our specific needs. And it’s very lightweight.
FilterStore. We hang a copy of the
FilterStore instance off
window so it’s accessible to everything.
Many of you may be groaning or decrying our flagrant disregard for best practices. Hanging things off
Window? Passing a reference to the top-level component’s
setState() function to a singleton, to be called from anywhere? Are we crazy? Crazy like a fox, I think.
Putting it All Together
With a simple global state container, a plan of attack for rendering, and an existing server based loading scheme, it was pretty simple to knock the actual functionality out. We opted not to use any third party components, and built everything ourselves. The state of third party React components is pretty dismal when you factor in maintenance, dependencency management and extensibility. We saved many hours avoiding these common pitfalls.
One architectural decision we made was to lean towards larger class-based components instead of dozens of stateless function components. There was less cognitive overhead and indirection when the entire section of UI is all in a single file. They do get longer, and there is a bit more per-component state management, but so far maintenance and extension have been much easier. No need to wrap a
<button> with a
<TrackJSButton> component, or some of the other stuff I commonly see.
The last thing worth mentioning is that we used “traditional” CSS to style the components. We are not big fans of CSS-in-JS. There are a vanishingly small number of reasons to consider any CSS-in-JS framework, and we didn’t have any of them. The upshot is that we now have an easily maintainable stylesheet instead of our styles spread across a dozen files.
If you haven’t tried TrackJS since the introduction of our new filtering capabilities, or you’ve never tried it, please consider signing up for a free trial!