1. React is not an architecture
As modern libraries go, React has remarkably minimal API surface area. This is greatly to its credit: it handles a small set of closely-related concerns and avoids the weird mixture of egoism and insecurity of do-everything frameworks. It’s a toolbox starter set, instead of a custom-built multi-tool. This can be confusing for developers used to the guardrails of frameworks that come with neat buckets for all their code—controllers here, data entities there—and an all-encompassing metaphor for how those pieces fit together.
I once worked with an engineer who believed, rather perversely, that Ruby being so thoroughly and dynamically object-oriented meant that the lessons of the last 40 years of object-oriented development no longer applied. You didn’t need the principle of interface segregation when you could just use metaprogramming to change the interfaces on the fly; and global function spaghetti was no longer global function spaghetti if the functions were written, laboriously, as classes with a single
call method. His code was some of the worst I’ve ever dealt with, not because he was stupid, but he overfocused on maximizing what he could do, and let himself be convinced that, in this brave new world, none of the old wisdom—mostly concerned with should instead of could—was relevant.
“Everything is a component” is to React as “everything is an object” is to Ruby: trivially and misleadingly true. The error is in seeing this as an end, when it’s actually a beginning: closer to a language extension than an easy-bake app toolkit. Like many libraries or frameworks, React’s quick out-of-the-box successes seem to bely the emergent complexity of real-world projects at scale. Simple apps or proofs-of-concepts can often be built out without much thought for any additional structure, creating the impression that no additional structure is required. (That’s usually the case, generically: if libraries didn’t automatically reduce complexity and make certain tasks easier on the local scale, at least, then they wouldn’t see any use at all.) The avoidable conceit is that because React’s simple and clean design requires only one kind of sprocket, then one kind of sprocket is all that you or your application should have.
It’s incumbent on the developer to anticipate the need for additional structure, and to resist the illusory simplicity of “a component is a component is a component.” A Ruby application that consists of just subclasses of
Object without any architectural patterns will devolve into code spaghetti quickly, and so will an application that uses React without drawing any distinctions between different flavors of components. That entails, for instance, drawing a meaningful distinction between something like a generic
Button component, on one hand, and a top-level
Signup screen, on the other. It extends to seeing built-in helpers like the
useReducer hooks from the same perspective: as low-level tools that could be splattered like semicolons all across your codebase, but probably shouldn’t be, if they’re not contributing to good software architecture and code design.
Ultimately, the simplicity of React itself is not a get-out-of-engineering-free card. On the contrary, the onus is on developers and teams to implement generally applicable patterns and principles that will serve us just as well in every future project, and which can be quickly grokked, in the abstract, by new team members.
2. React is a library for creating user interfaces
Zoom out too far and this becomes a bit tautological: in one sense, your entire front-end app can be seen as just a user interface for your back-end API. On its own terms, however, React is handling a familiar set of concerns for the front-end: rendering/compositing graphics primitives, and event-driven user input. In other words: it’s a view layer. Logic bound up too tightly in a view layer can be hard to reuse and is notoriously difficult to test, and the interface itself can be subject to dramatic changes, so it’s important to constrain the scope of your components’ responsibilities in much the same way as you would a custom
UIView in a native iOS application.
No, that doesn’t mean building out parallel hierarchies of “view controllers” or such, but it does mean that care should be taken to ensure that data manipulation, domain and business logic, and so on, are appropriately separated and abstracted from the user interface code. Common lower-level patterns like Redux go a long way here, but there aren’t any intrinsic guards against just falling into the habit of treating your application and your user interface state as one-and-the-same, and without a modicum of care they can end up just as tightly coupled as inlined logic would be.
Furthermore, the idea isn’t that application logic should be separated from the user interface because you might change the entire underlying system (i.e., swap out React for Ember) on a whim, but because the two concerns themselves should be easy to change on their own terms. This is exactly the same as maintaining a common, generic API backend that can support scripting, a web interface, and multiple native apps. They’re not separated so that they can be completely replaced on a whim, they’re separated so that they can evolve—in ways small or large—with minimal knock-on effects for other systems.
A simple rule of thumb is that if code isn’t managing presentation or handling events, then it probably shouldn’t be baked into a React component. That said: what happens at runtime is potentially a different story. The crucial point is that your code design should prioritize separation of concerns, but ultimately every program is a final, hard coupling of its substructures and dependencies; the fact that a library like
react-redux, for example, ties everything back together into a meta-structure of React components isn’t the issue, the issue is when you sacrifice the ability to separate your application code from React, at any level, even conceptually, you’re increasing the carrying cost, and sacrificing flexibility, extensibility, and reliability, for no good reason.
3. Don’t wage a battle against HTML and CSS
Fact is: the vast majority of apps that use React will never run on any platform other than HTML and CSS in a current web browser. If we exclude React Native apps, that can just be taken to be 100%. Ignoring the finer points of HTML and CSS, or trying to abstract them away entirely, serves little purpose, but can lead to a lot of extra work and maintenance costs.
Even developers who weren’t around for the early days of the Web when
<table> was used primarily for page layout can take the kneejerk aversion to tables way too far, to the point of re-implementing
<table> as a raft of components, consisting of a nest of
<span>s, and a raft of finicky, potentially buggy custom CSS. It’s really quite simple: if your data or interface is tabular, then you can just use a
<table>, that’s what it’s there for. If you want to lay things out in a grid that aren’t actually related to each other two-dimensionally (i.e., the rows and columns aren’t semantically meaningful and orthogonal) then, duh, don’t use a
<table> for that. Not only does
<table> handle most of your markup and positioning for you, it’s also more readable, and the built-in semantics are used by browsers and screen readers to enhance accessibility, automatically, without finnicky ARIA configuration.
It ought to go without saying, but any UI designers on the dev team should also know HTML and CSS and be familiar with the default behavior and known limitations, so that they’re not pushing UI embellishments that are easy to whip up in Sketch in lieu of proposals that would work just as well but with a much simpler implementation. Trying to abstract the developers themselves—so that the designers’ Sketch files can be treated, purely on their own terms, as an abstract, gold-standard hand-off from one team to another—is far worse, and leads to an explosion of complexity, byzantine UI hacks, a general breakdown in code quality, and immense developer frustration.
Key to succesful application development, with or without React, is knowing your materials, and having a sense of what is high-value-for-effort innovation and what is low-value development hell (custom scroll bars, custom drop shadows, pretty much anything else that can be summarized as “a custom-built version of some browser-provided functionality so that it can be styled slightly more flexibly.”) Finding the right balance between abstractionism and reductionism can sometimes be tricky, but that, much more than knowing the syntax of any given programming language, is what developers do.
4. SVG icons don’t need to be React components
On the flip side, the fact that React works by taking XML-style syntax and using it to render and update a DOM tree can be irrelevant implementation detail and a costly red herring. If you’re using it for something other than rendering your app’s user interface, then there’s probably a better, more efficient tool, ranging from a simple template string on up to a purpose-specific library like D3. Purity as a development priority is meaningless, here: there aren’t any prizes for unnecessarily baroque solutions that only serve to maintain the comfort blanket of a universal API. (Full disclosure: I’m totally guilty of having written a kludgy port because I didn’t really understand why an interface that made sense in one paradigm didn’t make any sense in the other. More on that another time.)
A particularly common form of React abuse is to look at a set of SVG icons, to see that SVGs are XML documents, to note that React supports rendering JSX to SVG, and then, finally, to convert all static SVGs to React components. Please do not do this: the fact that an image file format’s underlying implementation is XML does not mean you want to be rendering those files as React components. Browsers are perfectly capable of loading and rendering graphics files all by themselves, in several different ways, whether that’s a PNG or an SVG. The underlying XML format is an utterly irrelevant implementation detail, and using the React hammer on that particular nail doesn’t do anything but worsen performance and limit expressive power.
That doesn’t mean there aren’t any uses for generating an SVG document in a component, but if you wouldn’t replace
icons/email.png with an
EmailIcon component that drew the icon procedurally, with a series of curves and stroke operations, to a
<canvas>, then why would you want to do the equivalent to
icons/email.svg? Leave your SVG files as SVG files. Even if you think you have a reason not to, look for alternatives.
For example, it’d be much simpler to have 4 static arrow SVGs than to have one
ArrowIcon component that takes a
direction property and applies a rotation transform to the path. Likewise, for outline or filled versions: the absolute last thing you want to be doing is tweaking stroke thickness via a component’s props. That way lies madness. Leave the SVG-tweaking responsibilities to Sketch, lest the first change request come in a week asking if a certain icon can have one part be stroked but another part be filled, and then the next one asking if another icon can have two stroke widths, ad infinitum. That doesn’t sound so bad? Well, now we have separate stroke and fill versions of icons, but only for half of them—React can handle that, right? Don’t fall into this trap.
Maybe handling SVG icons as generic image assets seems less flexible in terms of what you can do with any given specific icon, but that’s a good thing, because, in the general case, you don’t want that flexibility, and it is ultimately illusory: in 99.9% of cases, there’s nothing you can do with a per-icon React component and stylesheet that can’t be handled in the icon SVG files themselves, as a graphic design concern, using a real vector graphics editor. Furthermore, the same set of icons ought to be usable universally, in other applications, email templates, or promotional materials. Do yourself a favor, and make less work for yourself by leaving SVG files as SVG files.