I have been using React for 8 years now, since the dawn of React 0.13.0
back when Bower and Grunt is still all the rage, up until today's React 18.2.0
where server components are introduced.
Over these eight years, I have noticed that JavaScript/React libraries, APIs, and design patterns tend to be introduced very optimistically as an immediate solution to a common problem. While they seemed straightforward at first, their complexity later rears its head in production when multiple moving parts of the application interact.
In many cases, these complexities can completely cripple a codebase.
As time goes by, problems in existing solutions are slowly uncovered and highlighted as the patterns gained wider adoption. New solutions are developed to address those issues. Awareness precedes options, and options precede changes. Yet, these solutions introduces further problems and the cycle repeats.
This generally applies to most patterns and libraries found in React codebases from the beginning of time, from render props, higher-order components, React hooks, recompose, ramda, redux-thunk, redux-saga, MobX, to server components. Again, the libraries themself might not be complex on its own, rather it's the interlink that results in complexity.
To ground our analysis to be a bit more concrete, I will use these terms to analyze and describe what I have observed in real codebases, both mine and others.
- Complexity
- Indirection
- Familiarity
- Ease of Debugging
- Cognitive Overhead
- Testability
First, let's go into what these terms means.
Complexity
I love how The Grug Brained Developer talks about complexity.
apex predator of grug is complexity. complexity very, very bad. given choice between complexity or one on one against t-rex, grug take t-rex: at least grug see t-rex
one day code base understandable and grug can get work done, everything good! next day impossible: complexity demon spirit has entered code and very dangerous situation! demon complexity spirit mocking him make change here break unrelated thing there what!?!
I'm sure most of us can sense complexity. You're trying to change a simple thing, but you have to update 10 places and everything breaks.
Rich Hickey's famous Simple Made Easy talk puts this elegantly.
- To complect is to tie things together. Complexity happens when many things are tied together. Simplicity happens when things are not tied together.
- Things that looks easy can be very very complex under the surface, as they tie together many things.
When you look at React patterns or libraries that looks simple, it's also worth understanding how that could add complexity to your application. Even if the abstraction seems really familiar, concise and easy to use, there might be an underlying complexity waiting to rear its head.

Personally, I found the theory of complexity fascinating. The following is from The Computational Beauty of Nature, one of my favourite books on this topic.
A complex systems with emergent properties are often highly parallel collections of similar units.
Even if you have 100 small, simple features that are isolated, there wouldn't be a lot of overall complexity if those features do not interact together.
This is why games with multiple systems that interacts tends to produce complex, interesting behaviour. When the weather is rainy in Tears of the Kingdom, the weather system interacts with the terrain and stamina system, and suddenly it is so difficult to climb mountains. Yet, it can be difficult to pinpoint the source of bugs in these complex systems.
Now, a question for you to think about. Is the React library by itself complex? What units of systems does React complects together? (e.g. Virtual DOM, Reconciler, Scheduler, DOM Renderer, Cache, Hooks, JSX). I think it's very insightful to peer into how a library is implemented, to understand why the complexity is there, and what alternative design choices exist.
Indirection
Imagine you have a button, which calls a function, which calls another function, which calls another method in a class, which calls yet another function, and so on.
If there is a bug related to this button, how do you know which class or function caused the bug? You would have to debug or bisect it.

Familiarity
Sometimes, things that are unfamiliar to us appears complex, even though they might or might not be so.
Most people learned programming in mostly imperative paradigms, eg. C, C++, Go, or Python.
However, people coming from Lisp or functional backgrounds may find certain concepts easier to grok.
Ease of Debugging
How easy it is to log, to bisect, to divide and conquer, to add breakpoints, to understand where the culprit of an issue comes from, to know what are the invariants?
Another property to consider is Observability
Cognitive Overhead
Needing to do things manually. Dependency tracking. Grouping Things Naming Things Edge Cases Leaky or Greedy Abstractions - Less Power, Easier mental model
How many place do we have to update in the codebase? Boilerplates.
Incongruence in mental models. Additional cognitive overload.
- Edge Cases to Remember / Mental Model Incongruency
Testability
What to trust - what not to test What to test Where should we test (integration or unit) How easy it is to test How do we test it? (Avoid mocks)
Fog of War
- Historical context
- Some solutions solve problem, but they create new ones (as indicated above)
Recompose and HoCs
- Introduces layers of complexity
- Trees
Hooks
- Manual vs auto dep track (svelte, angular, vue, solid)