A Modular Approach to UI Problem Solving
At React Rally last year, I watched an excellent talk by Lin Clark called “Making small modules actually work with webpack and npm”. At the beginning of the talk, Lin called out some participants in browserify vs. webpack arguments for their condescending remarks about authentic “modularity,” who sucks and who doesn’t. It’s been suggested that webpack (and other mono-repos, like React) are not “modular” because they are not split across separately packaged repositories; in response, Lin pointed out that the core of modular programming is an approach to problem solving, not necessarily an approach to packaging. And it’s the problem-solving part that’s most important.
She explained that modular problem solving is about dismantling bigger problems into smaller problems, then solving those smaller problems with individual modules that are highly cohesive and loosely coupled. Each module should contain everything necessary to solve a particular, focused problem, and then provide a public interface that makes it usable by an indefinite variety of other modules, in an indefinite variety of contexts.
If Problem A can be broken down into Problems B, C, and D, then you might consider creating Modules B, C, and D, each solving their own particular problems, upon which Module A can depend. Module A then becomes pretty small. And when you run into Problem X, you might realize that it also involve Problems B and D, so you can reuse those modules.
Everybody is their own little world, of course, so you may disagree with this statement — but in my experience, it seems like not much effort has gone into modular problem solving for user interface components. Most UI libraries present themselves as complete drop-in solutions — the keyword being “complete.” These complete drop-in solution can be as small as a tooltip or as large as jQuery UI or Bootstrap.
I can think of many reasons why this pattern may have developed, and I’m sure more knowledgeable and experienced developers can think of even more. But the frontend realm has evolved in such a way that it’s high time we start experimenting with more modular approaches.
The time is ripe
Dependency management and bundling
Until recently, the more small scripts you loaded, the slower your site would be; and the more dependencies any given script relied upon, the harder it would be to use. <script>
tags, global variables, and weak or non-existent build processes meant that we had good incentives to write and use big libraries that tackled as many problems as possible within a single codebase.
But dependency management and bundling are no longer significant impediments for developers versed in npm and bundlers like browserify and webpack. As everybody should be. (The rest of Lin Clark’s talk was about that.)
Let’s get sophisticated
Because of the way that web development can blend into graphic design and Wordpress and super-fast site churn for small clients, etc., there are many consumers of UI components that do not want to know what problems need solving or the best way to solve them: they just want a drop-in fancy-looking Thing.
I can see how plenty of people would need complete drop-in solutions for the kind of work they do. But frontend development has become increasingly complex, and there are more and more frontend developers who are technically minded, who want to pick apart their problems and want to consider a variety of possible solutions, and want or even need to customize their approach to a unique context. For us, drop-in solutions are becoming less and less appealing, if not out of the question altogether, most of the time.
My attempts at modular UI libraries
I’ve been working on a different approach while building a few open-source React components. Instead of constructing complete, drop-in, strictly React-focused solutions, I’ve tried to isolate problems and extract small JS libraries (ideally vanilla JS) to solve those problems; then include them as dependencies in the React component.
So while developing these three React components …
… I’ve published these lower-level vanilla JS libraries:
… and these lower-level React libraries:
I wrote this little post to explain why I did that.
Modular problem solving means better solutions
When you’ve opened a modal, you shouldn’t be able to scroll the main page until you’ve closed that modal. This is a basic virtue of any decent modal. So every drop-in modal library out there that aspires to decency must implement its own solution to that problem (scroll-stopping). And because each implementation will be different, some will be great, some will be good, and some will be bad.
One of the important niceties of a scroll-stopper is that it prevents the awkward horizontal bounce that can happen when the scrollbar disappears and reappears as modals open and close. This is a small but somewhat tricky problem: you have to detect whether the scrollbar is present or not, and if it is determine its width (which varies among browsers and operating systems), then alter some CSS styles to compensate and un-compensate as the scrollbar disappears and reappears.
Plenty of modal libraries don’t do this at all or don’t do it very well. It would be better, of course, if those bad implementations willingly gave up their claim to life, and their host libraries adopted a superior scroll-stopping solution shared by one of the other modal libraries. A scroll-stopping module.
When building react-aria-modal, I wanted to stop the scroll the best way I could, and at the same time make my solution available to others, making it easy for them to do the same thing at least as well as I could. So instead of writing my scroll-stopping logic directly into my React component, I made no-scroll.
Here are what I consider to be the benefits:
- no-scroll focuses on solving one problem the best way I know how, without getting tangled up in other modal requirements.
- Other people building other modal libraries can use no-scroll instead of reimplementing the same (or worse) logic in their own codebase.
- If other people know of better ways to solve that one problem, they can help out Me And All The World by improving no-scroll, thereby improving react-aria-modal and anything else that uses no-scroll.
- Use-cases other than modals might exist for scroll-stopping; so other libraries and components that stop scrolling can also depend on no-scroll and share the solution.
Modular problem solving means reusable solutions
Another problem with modals is that they work best when they are dynamically appended to the end of the <body>
tag, instead of living in the DOM tree right next to whatever trigger invokes them.
This isn’t really a problem in vanilla JS, but is a problem in React. So I didn’t want to separate the solution from React, but still wanted to separate it from the modal components, because I knew that it would be handy in other components. (For example, a loading spinner that overlays the whole screen is also better off if appended to the end of the <body>
.)
So I made react-displace, which I use in react-aria-modal and also use in some project-specific code for loading spinners and other things.
Modals also need to trap focus. That’s not a React-specific problem at all. So I made focus-trap to handle that in vanilla JS, so it could be used by other modal libraries and also by other UI components that may want to trap focus, like popovers with forms in them or who knows what else.
And so on. By extracting a problem and generalizing its solution you can ensure that the same (hopefully good) solution is easily available for you when that problem rises again, maybe unexpectedly, in another context.
The diversity of JS frameworks makes low-level libraries especially appealing
I’m using React a lot now, and liking it. But I do understand why other people are using Angular, Ember, web components, whatever else, no big weird framework at all, and so on. And I also know that I’ll probably be using some other component-creating-JS-tool in the future.
A React menubutton, no matter how good, is not the only menubutton that the world or I myself will need. There is not and will never be a menubutton to end all menubuttons.
Let’s say that I pull off a truly fabulous menubutton component in React, for users of React. That’s swell for Current React-Using Me, and swell for other React users; but if all the important code is written in a React-specific manner, it sucks for non-React users and for Future Me. For them, my fabulous React menubutton is about as useless as a crappy React menubutton, or none at all. When a Non-React User or Future Me tries to create a new, equally fabulous menubutton component in the UI framework of the future — let’s call it KrazyKomponents — they’re going to have to reimplement whatever made my React widget fabulous. And then if they improve their KrazyKomponents menubutton with Rad Feature X, my formerly-fabulous React component falls behind — unless I put in the labor to write my own React-specific implementation of Rad Feature X.
And so everybody loses, forever.
With react-aria-menubutton, I tried to isolate the hard problems into a vanilla JS library that I and others could re-use, even in non-React menubutton components (or, even further, in non-React, non-menubutton components). Hiding and showing stuff isn’t very hard. Neither is toggling ARIA attributes. Those problems are trivial in the React code, and probably will be in any other decent view library (current or future). But the keyboard interactions are more tricky. The up and down arrows should move focus through the items, wrapping from top to bottom and bottom to top; and it’s all so much better if your user can type letters to skip to specific menu items. So instead of solving those keyboard-interaction and focus-management problems with React-specific code, I wrote focus-group in vanilla JS, which provides an API enabling that kind of menu-y behavior.
Somebody writing ember-menubutton, or angular-menubutton, or krazykomponents-menubutton can use focus-group to get the same sweet keyboard interactions that I worked hard on for react-aria-menubutton. With those keyboard interactions taken care of, the rest of their framework-specific menubutton code might be pretty trivial, really.
Additionally, if those Ember, Angular, or KrazyKomponents authors figure out better keyboard interactions, we can update the focus-group library and react-aria-menubutton wins, too.
Everybody can win, at the same time.
(By the way: The earlier versions of react-aria-menubutton had not extracted this solution into focus-group. So in react-aria-tabpanel I implemented very similar logic that also involved left and right arrows keys, but did not bother doing anything with letter keys, because that’s kind hard. Now, both UI components use focus-group, which means both have the same quality of keyboard interaction and I was able to add letter-key navigation to react-aria-tabpanel without any additional strain and suffering.)
So that’s something worth trying, right?
I think so.