What is React? A Complete Guide to the Popular JS Library

$ du -sh ./node_modules
1.2G ./node_modules
$ du -sh ../rtos_project/src
420K ../rtos_project/src
$ npm install
npm ERR! code ERESOLVE
npm ERR! ERESOLVE could not resolve dependency
npm ERR! Conflicting peer dependency: [email protected]

$ node -v
v21.7.1

I’ve spent the last two decades optimizing interrupt service routines to fit within 50-microsecond windows. I’ve fought for every byte of SRAM on an STM32. Now, management has decided that our industrial controller needs a "modern, responsive web dashboard." They’ve handed me a MacBook Pro with 32GB of RAM and told me to use React. Within twenty minutes, I watched a "Hello World" application consume more memory than the entire flight control system of a Boeing 747. 

The web is a graveyard of engineering principles. We’ve traded deterministic execution for a stack of abstractions so high that no one actually knows what’s happening at the silicon level anymore. This is a log of my attempt to understand why we are doing this to ourselves.

## I. The Document Object Model: A Structural Failure

Before we even get to React, we have to talk about the DOM. In any sane system, if you want to update a pixel, you write a value to a memory-mapped register or a framebuffer. In the browser, you have the DOM—a bloated, tree-based data structure where every single node is a massive C++ object. 

When you change a single piece of text in a `<span>`, the browser doesn't just update a character buffer. It triggers a cascade. It recalculates styles. It recomputes geometry. It repaints layers. It’s an O(n) operation where 'n' is the complexity of the entire UI. For someone used to O(1) register writes, this is offensive. 

React claims to solve this by adding *another* layer of abstraction on top of the already broken one. This is the "Virtual DOM." Instead of touching the slow DOM, you touch a "fast" JavaScript representation of it. But "fast" is a relative term. In the world of Node.js v21.x, "fast" means "only slightly slower than a turtle with a limp."

## II. The Reconciliation Tax and the Fiber Scheduler

React 18.3.1 uses something called the Fiber reconciler. To an embedded engineer, this looks like a poorly implemented cooperative multitasking kernel running inside a single-threaded virtual machine. 

When state changes, React doesn't just update the screen. It starts a "render phase." It builds a new tree of "Fiber" nodes. Each Fiber node is a heap-allocated object containing pointers to its parent, child, and siblings, along with a "memoized" state and a set of "update queues." 

**What is** the sanity behind maintaining two entire trees of objects—the "current" tree and the "workInProgress" tree—just to decide that a button's color changed from #FFF to #000? 

The "Diffing Algorithm" is the core of this tax. It’s a heuristic approach to the tree-to-tree transformation problem. Since the general problem is O(n^3), React settles for O(n) by making assumptions. It assumes that two elements of different types will produce different trees. It assumes you’ll provide a "key" prop for list items so it doesn't have to guess which element moved where. 

In C, if I have an array of structs and one changes, I know the index. I update the index. In React, the framework iterates through the old tree and the new tree, comparing types and keys, trying to figure out what changed. It’s a massive amount of CPU cycles spent rediscovering information the developer already knew. 

## III. JSX: The Transpilation Nightmare

We used to write code that the compiler turned into machine instructions. Now, we write JSX—a bastardized marriage of HTML and JavaScript—that must be "transpiled" by Babel or SWC before it can even be parsed by the V8 engine.

```javascript
// What I write:
const Component = ({ label }) => <div className="btn">{label}</div>;

// What the browser actually gets (roughly):
const Component = ({ label }) => React.createElement("div", { className: "btn" }, label);

Every time that component “renders,” React.createElement is called. That’s an object allocation. Every. Single. Time. If your component renders 60 times a second (the “smooth” UI dream), you are flooding the heap with short-lived objects. You are practically begging the Garbage Collector to step in and freeze your execution thread.

In an RTOS, we avoid heap allocation during critical loops. In React, the entire philosophy is built on heap allocation during the UI loop. We’ve replaced the stack with a garbage-collected heap and then wondered why the “frame rate” of our web apps feels like a slideshow on anything less than a Core i9.

IV. State Persistence vs. Volatile Memory

In a microcontroller, state is simple. It’s a variable in the .data or .bss section. It stays where you put it.

In React 18.3.1, state is managed through “Hooks.” Let’s look at useState.

const [count, setCount] = useState(0);

This looks like a simple assignment, but it’s a lie. useState is a pointer into a linked list of state cells attached to the Fiber node currently being processed. This is why you cannot call hooks inside loops or conditionals. If the order of hook calls changes between renders, the pointer points to the wrong “cell,” and your state is corrupted.

It’s a manual memory management system disguised as a high-level API, but without any of the safety of a real pointer system. If I mismanage a pointer in C, the CPU throws a HardFault and I can debug the stack. If I mismanage a hook in React, the UI just displays the wrong data or enters an infinite re-render loop that pegs the CPU at 100% until the browser tab crashes.

V. The “Concurrent Mode” Illusion

React 18 introduced “Concurrent Rendering.” This is the framework’s attempt to solve the “jank” caused by its own overhead. It uses the MessageChannel API to yield execution back to the browser’s main thread so the UI doesn’t freeze during a heavy diffing operation.

It’s a scheduler. It assigns priorities to different updates. DiscreteEventPriority for clicks, ContinuousEventPriority for mouse moves, DefaultEventPriority for data fetching.

This is exactly what we do with task priorities in FreeRTOS. Except, in FreeRTOS, the context switch takes a few hundred nanoseconds. In React, the “context switch” involves the Fiber reconciler pausing its tree traversal, saving its place in the workInProgress tree, and letting the browser’s event loop run.

The complexity is staggering. To support this, every piece of state must be immutable. You don’t mutate an object; you clone it with the change.

// The "Modern" Way
const updateSettings = (newVal) => {
  setSettings(prev => ({ ...prev, volume: newVal }));
};

That { ...prev } is a shallow copy. It creates a new object. If prev has 50 fields, you just allocated a new object with 50 fields to change one integer. We are burning through RAM to maintain “immutability” so that the scheduler can keep track of which version of the state it’s currently rendering. It’s a massive memory-for-convenience trade-off that would be laughed out of any embedded systems review board.

VI. Synthetic Events: A Wrapper Around a Wrapper

React doesn’t use the browser’s native event system. It uses “SyntheticEvents.” It attaches a single event listener to the root of the document and then dispatches events itself.

Why? Cross-browser compatibility. In 2024.

We are still paying the performance tax for Internet Explorer 6’s non-standard event model. Every click, every keypress, is wrapped in a new SyntheticEvent object, pooled (sometimes), and dispatched through React’s internal propagation logic. It’s a software-level implementation of an interrupt controller, built on top of the browser’s already existing interrupt controller.

If I wrote a GPIO driver that added three layers of abstraction between the pin toggle and the CPU, I’d be forced to write a formal apology. In React, it’s just “how it works.”

VII. The Dependency Hell of Node.js v21.x

To even begin building this “dashboard,” I had to run npm install. This command downloaded 842MB of JavaScript files into a folder called node_modules.

I checked the package-lock.json. We have 1,200 dependencies. To display a graph and three buttons.

One of these dependencies is is-number, which is a two-line function to check if a value is a number. Another is is-odd. These are things that are built into the language, or should be. But the “modern” developer prefers to pull in a third-party library rather than write a bitwise AND operation.

The security implications are a nightmare. Each of these 1,200 packages is a potential vector for a supply chain attack. I’m used to auditing every line of code that goes into a firmware image. Here, I’m importing code from “fancypants-ui-guy-99” just to get a CSS transition to work.

And the build tools! Vite, Webpack, Rollup, Esbuild. They are all trying to solve the same problem: JavaScript is too slow and fragmented to be used as-is. So we use a tool written in Go or Rust to bundle our JavaScript so it can be shipped to a browser that uses a C++ engine to turn it back into machine code.

VIII. Why “Why React?” is the Only Question

When you ask “what is React,” the documentation tells you it’s a library for building user interfaces. That’s a lie. React is a workaround for the fact that the web was never designed to be an application platform.

The web was designed for linked documents. We’ve tried to turn it into a real-time application environment, and React is the scaffolding holding up the collapsing structure.

We use it because we’ve lost the ability to manage complexity ourselves. We’ve become so accustomed to infinite resources that we no longer care about the cost of an abstraction. We see 1.2GB of node_modules and 200MB of heap usage as “the cost of doing business.”

But there is a real cost. It’s the latency. It’s the battery life on the user’s device. It’s the sheer fragility of a system where a minor version bump in a transitive dependency can break the entire build.

IX. The Memory Footprint of a Single Component

Let’s do a teardown of a simple “Counter” component in React 18.3.1.

import React, { useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log("Count is now:", count);
  }, [count]);

  return (
    <div className="container">
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

In a microcontroller, this is:
1. A 4-byte integer in RAM.
2. A function that increments that integer.
3. A loop that updates a display buffer.

In React:
1. The Function Object: Counter itself is an object in the JS heap.
2. The Hook Linked List: A useState record and a useEffect record are created. The useEffect record stores the “effect” function and the “dependency” array.
3. The Virtual DOM Nodes: A div object, a p object, and a button object are created on every render.
4. The Synthetic Event Closure: The onClick handler is a new function created on every render, capturing the count variable in its closure.
5. The Fiber Node: A Fiber node tracks all of this, including pointers to the “alternate” Fiber for the next reconciliation.

Total memory for a counter? Easily several kilobytes of heap, not counting the overhead of the V8 engine itself. For one integer.

X. The Rehydration Problem

Then there’s “Server-Side Rendering” (SSR) and “Rehydration.” Because the JavaScript bundle is so large (several megabytes), the user has to wait too long to see anything. So, we run the React code on the server, generate HTML, send that to the browser, and then—this is the kicker—we send the same JavaScript to the browser so it can “rehydrate.”

Rehydration is the process where React walks the existing HTML DOM and tries to attach its internal event listeners and state to it. It’s essentially running the entire initialization twice. It’s like shipping a pre-assembled machine to a customer, but then sending a mechanic to take it apart and put it back together again just to make sure the “state” is correct.

It’s an admission of failure. We’ve made our tools so heavy that we need to perform architectural gymnastics just to make them feel “seamless” (a word I use here with utter contempt).

XI. The “Effect” Trap

useEffect is where engineering goes to die. It’s a “synchronization” primitive that developers use for everything from data fetching to manual DOM manipulation.

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

If you forget that return statement (the “cleanup” function), you have a memory leak. In an embedded system, a leak eventually hits the top of the stack and crashes the system. In a browser, it just makes the fan spin faster until the user closes the tab.

The “dependency array” is another source of constant bugs. If you miss a variable, you have a “stale closure.” You are looking at a version of the variable from three renders ago. It’s like reading a register and getting the value from five minutes ago because the bus decided to cache it without telling you.

XII. Conclusion: The State of the Art

I have been forced to build this dashboard. I have used npm. I have written JSX. I have watched my RAM disappear into the void of node_modules.

We have built a world where the simplest tasks require the most complex tools. We have replaced understanding with “best practices.” We have replaced efficiency with “developer experience.”

React 18.3.1 is a marvel of engineering, in the same way that a Rube Goldberg machine is a marvel of engineering. It’s a brilliant solution to a set of problems that we created for ourselves by deciding that the document viewer should be an operating system.

I’m going back to my logic analyzer. At least there, when a bit flips, I know why.

“`bash
$ rm -rf node_modules
$ make clean
$ make flash
Target: STM32F405
Flash: 12.4KB / 1MB
RAM: 1.1KB / 192KB
Status: Success. Deterministic. Sane.

Related Articles

Explore more insights and best practices:

Leave a Comment