Comparing Web Philosophies for a UI Problem

Daniel Orner
8 min readSep 17, 2020

--

Image from Pixabay

The front-end ecosystem has exploded in recent years. Although the “winner” at the time of this writing appears to be React, it’s not the only possible solution, and is often used in cases where it shouldn’t be.

The technology being used isn’t as important as the philosophy — there are a whole bunch of component frameworks at this point, but they all use the same basic paradigm. React generates everything from the ground up — it can’t be easily “decorated” onto an existing HTML document, but is more aimed at “componentizing” everything about the application. There are other approaches (including Stimulus, which I’ll discuss below) which add interactivity to a static, already-rendered page, and still others which take a hybrid tack.

Here’s a case study of an unusual problem and a number of different approaches I attempted to use to solve it.

The Context

Image from PxFuel

Recently, I rewrote one of our internal apps. The app is essentially a set of forms with limited interactivity, but was written using React, Redux, and Sagas, which was significant overkill for that type of application. The rewrite used plain Rails ERB (templated HTML), using Turbolinks to automatically make the page transitions look nicer, and for the interactive parts, I went with Stimulus.

With React you build actual pages as “components”, Because of this, I’ve seen many cases where each page can have several page-specific subcomponents. In the extreme end, a mostly static page can be comprised of dozens of components, many of which are designed only for that specific page.

With Stimulus, since you’re generating HTML on the server side, there’s usually no need to make any page-specific controllers. I made a “toggle controller” (which toggles an element’s visibility based on the value of a drop-down), an “add row controller” (which adds a row to a table when a button is clicked), etc., and let the rendered HTML take care of the specific content.

With this approach, the total lines of code dropped by about 9,000 — that includes all the HTML and server code I added and the tests that didn’t exist that were created as part of the rewrite. The end result had less than 400 lines of JavaScript.

The Challenge

Image from Pixabay

Stimulus is a lot more flexible than React, which is a blessing and a curse. You can add your controllers literally anywhere as long as the correct attributes are set on your elements. But because of that, you don’t get that nice, boxed-in feeling of owning your entire component and defining how it’s used, or immediately understanding how to “call” it. There’s a limit you can reach fairly easily where you need more boundaries.

I mostly avoided that limit in my rewrite, but there was one task which seemed to bump up against it: A paginated, filtered table with static, client-side data.

The reason this had to be fully client-side is that rather than fetching data from a database, we were listing objects (services, functions, etc.) from AWS API calls. These API calls could only be tokenized, not paginated (you can’t say “give me page 4 of this API call”). So we couldn’t paginate this reasonably anywhere but the browser.

In addition, I wanted a generic solution, because this is a pattern that repeated multiple times throughout the application, and the tables and filters look slightly different each time. I didn’t want to create separate controllers for each of my pages when the functionality it’s trying to impart is practically identical.

Image from Pixabay

Without pasting the full solution, some sample HTML that could use my Stimulus controller might look like this:

This hooks up the individual input fields with the controller (which it uses both for event handling and reading the values), as well as the table itself. The rows are given HTML data attributes to determine what the filterable values are for each row.

The nicest thing about this is that it does not care what the rows themselves look like. The filters can also live anywhere on the page — they could be three layers deep in divs. We literally “sprinkle” the ability to filter and paginate our table onto any HTML we want.

Another advantage is that we get immediate reference to each element we need to interact with — we don’t need to query the DOM tree to determine where each of these elements live.

There are a couple of downsides, though. One is that all rows need to be rendered initially, and they are filtered as you navigate and type search text. This can introduce performance problems at a high enough scale. (Arguably if you get this far, you anyway need to move past this and introduce some kind of server-side caching and use real pagination.)

A bigger thing that rubs me the wrong way is the pagination piece. My Stimulus controller actually generates the navigation menu, which is against its normal philosophy. Unfortunately there’s simply no way around this — the controller doesn’t know in advance how many pages to display, and the pages in view will change as the user paginates and filters, so we have to recreate it from the ground up each time.

What about React?

This got me to thinking how React would handle this kind of case — where we want to have rows that could look arbitrary, and filters that could live in arbitrary spots in the page. More importantly, could React handle this outside a “React app”? Could we drop a React component into an HTML page and have it work? This is definitely not the “standard” React ecosystem, where you are expected to build everything from the ground up.

The idea in my head here was to have some kind of generic “Filters” component which could be swapped out to render different kinds of filters, and a generic “TableRow” component that could render the row. Thinking in object-orientation, it would be great to define an interface or “parent component” that is expected to behave a certain way, and then allow us to pass in “child components” that implement that behavior.

Making a “generic” or high-level component is possible in React, but most cases I’ve seen just take their props.children and re-render them as-is. This halfway point where the children are “arbitrary” but actually need to conform to a particular kind of behavior doesn’t fit in with normal use.

One way this might look is something like this:

Rendering the rows can be done in a somewhat straightforward manner by creating a function that returns some JSX based on the data it’s passed in, and calling that function when it’s time to populate the table. Filters, on the other hand, are a lot more complicated, particularly if they can be in arbitrary places. We have to make different child components for each layout type, and possibly for each page that we want this on.

We would need to define some kind of filter function for each filter, which has to be made available to the parent component by one of the multitudinous state patterns in React (context, Redux, useState, render props, etc.). Finally, we need to hook up all the event handling code correctly so the different layers are informed at the right times when the filters change.

This seems a lot messier than the Stimulus solution — the simplest case would likely need at least four or five components defined, I don’t like the look of rendering rows via a plain function, and it looks like we’d need to define new components for each type of filter and layout. It’s likely possible to get by without these things, but you’d need to go past common React idioms.

What about Web Components?

Web Components have a few advantages over React for this use case. They are significantly simpler and have fewer ways of achieving a particular result. They’re also available in all major browsers without needing libraries installed. More importantly, you can wrap arbitrary HTML in a web component— you can break into that encapsulation in ways that React doesn’t really allow.

A web component solution might be used as follows:

Because web components can inspect their children on startup, you can implement something like the ws-filters element to set up event handlers and collect filter information on its descendant input elements pretty easily.

For displaying the rows, you can have a component which exposes a render() method which allows you passing in a JavaScript object (in this case I called it ws-topic-row). You can use duck-typing so that any component that has a render() function can be used to render a row.

This is quite a bit cleaner than React because you don’t need to create separate components for every type of layout or filter. On the other hand, it’s not as open-ended as Stimulus because much of the behavior and impact is encapsulated in the web components. However, I don’t like how it has to inspect its children to find e.g. the filter elements — Stimulus is less brittle in this regard.

Conclusion

In the course of studying this problem, I’ve gotten a better understanding of the strengths and weaknesses of these three philosophies. Each tool tries to stress what they think is important:

  • React has a strong, encapsulated ecosystem where everything is a component, and allows for clean separation of concerns; with the downside of its difficulty to break out of that ecosystem when generalizing things;
  • Stimulus is very simple and focuses more on event handling and making changes to existing HTML, which doesn’t work as well when it’s absolutely necessary to generate new markup, and becomes confusing past a certain level of complexity;
  • Web components allow for extensibility and flexibility, at the expense of being neither as encapsulated as React nor as convenient as Stimulus.

Honestly, I feel like a hybrid between two approaches works best in this case —creating a web component for the navigation, getting the nice encapsulation and easy HTML generation, and allowing Stimulus to sprinkle its behavior for the existing table and filters, since they can look like anything.

I’m far from an expert in any of these technologies, so it was a fun experiment!

--

--