Florian Rappl Technology enthusiast and solution architect in the IoT space.

React is a black box. Why does that matter?

5 min read 1404

react-black-box-nocdn

React is arguably the most-loved frontend technology. One of the reasons for this success is undoubtedly React’s small API surface, which has grown in recent years but can still be learned in just a couple of hours.

Even though React’s API is small, many devs argue that React’s internals are not only quite complicated, but need to be known these days. So naturally, the question arises — does it matter that React is a black box? Does it help us, or does it impact us negatively?

In this article, I’ll explore the ins and outs of React’s abstraction model in pursuit of an answer.

React’s outside API

In many use cases, React’s outside API is pretty much nonexistent. If we write JSX like so:

const element = <div>Hello!</div>;

Or like so:

const Component = ({ children }) => (
  <>
    <p>I am a component</p>
    {children}
  </>
);

Then this is transpiled into a call to jsx from the react/jsx-runtime module. Even before the new JSX transform was introduced, all we had to do was to bring in React, such as:

import * as React from 'react';

const element = <div>Hello!</div>;

And a transpiler such as Babel or TypeScript would have transformed it to call React.createElement.

So we can see already that React’s most important API is pretty much hidden. With createElement or jsx being used implicitly, we never called the outside API explicitly.

Now, excluding more “classic” APIs such as Component or PureComponent (including their lifecycle), we know that React offers a lot more than we may want (or even need) to use. For instance, using lazy for lazy loading (e.g., for bundle splitting) capabilities is quite cool but requires a Suspense boundary.

On the other hand, we have APIs like useState and useEffect that bring in a certain magic. First, these are all functions, but these functions cannot be used just anywhere. They can only be used inside a component, and only when being called (i.e., rendered) from React. Even then, they may not behave exactly as we expect.

We made a custom demo for .
No really. Click here to check it out.

These are APIs that are quite leaky. To understand them, we need to have quite a sophisticated understanding of what happens inside of React — which brings us to the inside API.

React’s inside API

There are three kinds of inside APIs:

  1. APIs that are usually only implemented by a few libraries (such as the reconciliation API — more on that later)
  2. APIs that can sometimes be useful and reachable, but not stable, on the outside
  3. APIs that cannot be reached from the outside; they are (and can) only be used internally

I don’t want to focus on No. 3 above, as this is anyway beyond our reach. Going for No. 2 does not make much sense either, as these are always subject to change and should be avoided. Which leaves us with APIs that are implemented by only a few libraries but have quite some impact.

As previously mentioned, the most important thing to implement is the reconciliation API. One implementation of this is provided by the render function of react-dom. Another example is renderToString from react-dom/server. What’s the difference?

Let’s consider a more complex (yet still simple) component:

const Component = () => {
  const [color, setColor] = useState('white');

  useLayoutEffect(() => {
    document.body.style.backgroundColor = color;
  }, [color]);

  return (
    <>
      <p>Select your preferred background color.</p>
      <select onChange={e => setColor(e.target.value)} value={color}>
        <option value="white">White</option>
        <option value="black">Black</option>
        <option value="red">Red</option>
        <option value="green">Green</option>
        <option value="blue">Blue</option>
      </select>
    </>
  );
};

There are parts about this component that make it less trivial to use within different rendering options. First, we obviously use the DOM directly, though only in the layout effect. Second, we use an effect — and a special one (“layout effect”), at that.

Using the DOM directly should be avoided as much as possible, but as seen in the example above, we sometimes miss the right methods to do things differently. To improve the situation, we could still guard this line like so:

if (typeof document !== 'undefined') {
  document.body.style.backgroundColor = color;
}

Or use some alternative check.

That still leaves us with useLayoutEffect. This one is highly rendering-specific and may not exist at all. For instance, using the renderToString function, we’ll get an error when we use this Hook.

One possibility, of course, is to fall back to the standard useEffect. But then we need to know the (not-so-obvious) difference between these two. In any case, the when of the useEffect execution is as foggy as the re-rendering strategy of calling the returned setter from a useState instance.

Let’s use this chance to step back a bit and explore why we care about any of this.

Why should we care whether React is a black box?

Abstractions are useful, abstractions are nice, and clearly, abstractions make us more productive. After all, instead of caring about all the low-level details, we can be much more expressive and only deal with the building blocks relevant to solving a specific task.

But abstractions can also create problems. Sometimes they are too restrictive. Sometimes they hide important details. Sometimes they are too opinionated.

In the case of React, this is a problem. If we need to think about all of the (not-so-visible) edge cases when creating a component, then this is quite complicated. We desire readability. And one thing that is urgently required for this is to be able to reason about behavior just by looking at a component.

In the words of Jared Palmer:

Remember that React is supposed to take care of the “how” so that we can focus on the “what” of our apps.

Losing this aspect means losing one of the core attributes that made React so strong in the first place. Sure, there is a lot of debate about whether Hooks really make React more complicated or if somebody just had the wrong impression about React in the first place.

However, such discussions are actually arguments in favor of an increasing complexity. If the principle of least surprise is violated (even for just a minority), it is already a problem — maybe not a big problem, but still a problem.

Consequently, the reason we care about React becoming harder to understand is that this will introduce unnecessary debates, subtle bugs, and greater need for training. These are all counterproductive and will decrease our efficiency.

Finding the right balance between “Hey, this thing is just magic” and “Oh, I didn’t expect it would work that way” is crucial. Sure, many of us may want to be on the magic side of things, but in reality, there are different perceptions. And at the end of the day, it would be good to just have it working.

In the future, React will become even more complicated. With React concurrent mode, we’ll get another rendering mode that can drastically change depending on our components’ internal structure. Bugs and undesired behavior will strike again.

It is not yet clear how much confusion React server components will add. At least on the RFC, the rules state quite a few restrictions that need to be known:

RFC React Components Restrictions

While React gets more powerful, it also gets more complicated — that is undeniable. Yes, part of that complexity should have been known beforehand anyway, but apparently, it was optional in most cases; now it’s required for many cases.

Conclusion

React’s strength is also, to some degree, its weakness. The beautiful abstraction works as long as we know the rules of game. Anything beyond leads to surprising and potentially unwanted behavior.

Clearly, opening things like the reconciliation algorithm is what makes React so powerful, but we need to have some knowledge and discipline to be able to use different rendering options.

Using Hooks correctly is another area where React’s black-box system might be too opaque to fully grasp what’s going on. Nevertheless, personally, I think that the choices and design are worth the trouble. In the end, React’s programming model brings productivity and joy to developers — something worth preserving.

What are your thoughts? Is React too much of a black box, or is the amount of magic well-tuned?

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Florian Rappl Technology enthusiast and solution architect in the IoT space.

Leave a Reply