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 TrackJS UI is primarily server rendered. When users navigate inside our app, we use PJAX to asynchronously load the HTML for the next page from the server. We only load markup - all of the javascript and stylesheets are loaded on the initial visit, and stay resident in memory for all subsequent navigation. We get the speed and smooth UX of a single page app, but without all the JavaScript complexity. On the server we use ASP.NET MVC and the Razor templating engine to render the markup. This approach has worked well for over 6 years, and we continue to build most of the app in this fashion.

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:

Area highlighted in orange is the new global filter UI

Area highlighted in orange is the new global filter UI

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!

When you run a JavaScript error monitoring service you see first-hand how easily things break. It is notoriously difficult to refactor JS with confidence, and anyways, your code is running in end-user browsers where anything can happen.

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.

module.exports = {
    mode: "development",
    entry: "./index.tsx",
    output: {
        filename: "global-filter.js",
        path: __dirname + "/dist"
    },
    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",
    resolve: {
        // Add '.ts' and '.tsx' as resolvable extensions.
        extensions: [".ts", ".tsx", ".js", ".json"]
    },
    module: {
        rules: [
            // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" },

            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    }
};

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)?

Dirty tricks.

    window.renderGlobalFilter = (globalFilterData: GlobalFilterProps) => {
        ReactDOM.render(
            <GlobalFilter {...globalFilterData} />,
            document.getElementById("global-filter-root")
        );
    }

    window.unmountGlobalFilter = () => {
        var globalFilterElement = document.getElementById("global-filter-root");
        if (globalFilterElement) {
            ReactDOM.unmountComponentAtNode(globalFilterElement);
        }
    }

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.)

Because the JavaScript does not need to be re-parsed (remember, it’s only loaded the first time) the re-render is near instant. The HTML replacement and the rendering of the React component are done synchronously within the same event loop macrotask, so there are no UI flickers or re-paint issues.

Managing State

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. The 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.

    // FilterStore implementation
    class _FilterStore {

        private getState: () => Filter[];
        private setState: (filters: Filter[]) => void;

        // We call initialize() inside the top-level application component. (more on this later)
        initialize(getState, setState) {
            this.getState = getState;
            this.setState = setState;
        }

        // A sample of the FilterStore API.
        // Internally these can call getState() and setState() to affect changes and cause re-rendering.
        allFilters(): Filter[] {}

        addFilter(filter: Filter) {}

        removeFilter(filter: Filter) {}

        hasUnsavedChanges(tab: SelectedFilterBuilderTab) {}

        getProposedFilters(tab: SelectedFilterBuilderTab): Filter[] {}

    }

    // Note we export as a singleton, so it can be imported anywhere else and used without ceremony.
    export var FilterStore = new _FilterStore();

Our very simple store implementation.

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 the top-level component's constructor
    FilterStore.initialize(
        () => this.state.filters, // getState
        (filters) => this.setState({ filters: filters }), // setState
    );

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.

    // Now inside any child component we can, for example, retrieve a list of all filters
    var allFilters = FilterStore.allFilters();

    // Or we can add a new one (triggering re-render)
    FilterStore.addFilter(newFilter);

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.

Incidentally, this also works from non-React-aware JavaScript. We have various hooks in old vanilla JS that call in to the 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!