React's Design Philosophy
/ 16 min read
React’s Rendering Mechanism
Have you ever experienced typing in a web page where each keystroke causes a half-second lag? Or clicking a button and having the entire page become completely unresponsive, like a static image that ignores all your clicks?
In the past, React’s rendering had a fatal flaw: when setState
was called, the entire component tree would run its render functions. Even with the virtual DOM, this wasted considerable time. Components whose props
hadn’t changed, or even components that didn’t depend on any props
at all, would still be re-rendered and diffed by default. For large web applications, this flaw often caused noticeable stuttering.
In contrast, Vue inherently knows what values a component depends on, allowing it to precisely re-render at the component level.
The Root Cause of Stuttering
As you probably know, browser rendering and JavaScript execution compete for the same thread.
When the required frame isn’t ready during refresh (most commonly due to JavaScript long tasks blocking), that frame gets dropped, creating a stuttering sensation. To maintain basic smoothness, rendering-blocking tasks shouldn’t exceed 30ms, with 16ms being even better. Using requestAnimationFrame
can effectively align with refresh timing, giving JavaScript more generous execution time.
In React development scenarios, if developers haven’t deliberately optimized, the higher up the component tree a change occurs, the longer the rendering time. This is because the old diff algorithm (stack reconciler) was uninterruptible—once rendering started, the old and new virtual DOM trees would begin synchronous recursive comparison and DOM operations. This recursive operation lacked exit mechanisms, making it easy for large components to exceed 16ms and cause stuttering.
So how does React actually compress large component rendering time to under 16ms?
Fiber to the Rescue
Starting with React 16, React introduced React Fiber and updated the Fiber Reconciler. Its core algorithm treats each component in the component tree as a unit, running these units in batches. When React detects insufficient runtime, it yields the thread back to rendering. After rendering completes, it continues from where it was interrupted. So Fiber provides React rendering with the ability to pause, abort, and adjust priorities.

Additionally, the new rendering process is divided into two phases: Render and Commit.
Let’s first look at what Fiber actually is. It’s simply a JavaScript object. In this context, we only need to focus on three properties:
export type Fiber = { // ... return: Fiber | null; child: Fiber | null; sibling: Fiber | null; // ...};
Render is a single-threaded process of traversing Fiber, and this process is interruptible—something the old algorithm’s recursive mechanism couldn’t provide. The basic algorithm is as follows, achieving the effect of prioritizing child nodes, then sibling nodes when no children exist, returning to the parent level after processing siblings, continuing with parent-level siblings, until returning to the root node marks completion.
let root = fiber;let node = fiber;while (true) { // Do something with node if (node.child) { node = node.child; continue; } if (node === root) { return; } while (!node.sibling) { if (!node.return || node.return === root) { return; } node = node.return; } node = node.sibling;}

React schedules the Fiber Reconciler through a Work Loop. When time runs short, React needs to yield the JavaScript thread to respond to user interactions and page rendering. This scheduling algorithm was initially intended to rely on the requestIdleCallback
interface, but due to persistent browser compatibility issues, React implemented its own solution.
This brings an insight: the time-slicing approach of Work Loop can also be applied to long list rendering in Vue.
Commit occurs after a complete Render phase, collecting a series of operations on the real DOM and executing them all at once. This step is executed synchronously. Separating these two phases prevents UI inconsistencies that would occur if the Render phase was interrupted mid-execution. Because these two steps are separated and the rendering step is interruptible, methods like componentWillUpdate
used to be called twice, which is why they were later deprecated.
This demo shows a performance comparison between the old and new rendering mechanisms, with a dramatic difference.
Stack performance analysis:

Fiber performance analysis:

Fiber combined with the Reconciliation algorithm dramatically improves system responsiveness, with only a slight increase in overall rendering time—a trade-off that seems quite worthwhile.
The Philosophy of Functional Components
Fiber’s core is efficiently obtaining the new virtual DOM from render
, and React itself actively embraces functional programming—a perfect match. Taking advantage of the Fiber update, React introduced Hooks, fully committing to functional components.
Class component:
class Example extends React.Component { state = { count: 0 };
componentDidMount() { // Lifecycle method }
render() { return <div>{this.state.count}</div>; }}
Functional component:
function Example() { const [count, setCount] = useState(0);
useEffect(() => { // Replaces lifecycle methods }, []);
return <div>{count}</div>;}
Compared to class components, functional components’ main advantages are cleaner syntax, elimination of this
, and a better foundation for extracting common logic. Additionally, lifecycle functions are unified into side-effect hooks like useEffect
, which incidentally solves the componentWillUpdate
asymmetry problem mentioned earlier.
The characteristic of render functions is that they must be pure functions—given the same input, the output is always deterministic and unchanging, producing the same result even after running ten thousand times. This is what we call having no side effects. We can express this behavior with the formula UI = f(state)
.
This characteristic brings up a somewhat counterintuitive question: what will the alert
show?
export default function Counter({ init }) { const [number, setNumber] = useState(init);
return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); alert(number); }} > +5 </button> </> );}
Once you truly understand React’s underlying mechanisms and JavaScript’s characteristics, the result is clearly the old value. In the current render, due to JavaScript’s lexical scoping, number
is a fixed value. After setNumber
, re-rendering doesn’t happen immediately (it just enters the queue waiting for Work Loop scheduling), so even though alert
comes after setNumber
, it won’t immediately get the new result.
Introducing Hooks

This image is an excellent metaphor—Hooks are like hooks within functional components. Compared to earlier versions, after fully embracing functional components, all the various properties of class components needed new storage solutions. The introduction of Hooks allows these render functions to still possess React’s additional capabilities.
Since pure functions cannot record state, the essence of Hooks is to transfer state elsewhere, hooking into values outside the function through these hooks.
Why Hooks Must Be at the Top Level
When React transfers state, call order is crucial. If written inside if
statements or functions, the call order cannot be guaranteed.
Let’s look at Didact’s useState
implementation:
let wipFiber = null;let hookIndex = null; // Hook order storage
function updateFunctionComponent(fiber) { wipFiber = fiber; hookIndex = 0; wipFiber.hooks = []; const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children);}
function useState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // Get hook by index const hook = { state: oldHook ? oldHook.state : initial, queue: [], };
const actions = oldHook ? oldHook.queue : []; actions.forEach((action) => { hook.state = action(hook.state); });
const setState = (action) => { hook.queue.push(action); wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, }; nextUnitOfWork = wipRoot; deletions = []; };
wipFiber.hooks.push(hook); hookIndex++; // index + 1 for next hook return [hook.state, setState];}
Optimizations Based on the New Paradigm
For React’s new paradigm of functional components + Fiber, optimization approaches mainly come from two angles: caching and scheduling.
Caching
Compared to class components that run the render
function when updating, after switching to functional components, the entire function runs on every render. If this component contains large functions and CPU-intensive operations, it can easily cause performance issues.
As mentioned earlier, Hooks transfer state elsewhere. Besides state, it’s sometimes necessary to store large functions, dependent functions, and cache CPU-intensive operations.
This is where useMemo
and useCallback
become essential.
Check out this Demo:
import React, { useState, useMemo } from "react";
// A function to calculate the nth Fibonacci number (CPU-intensive operation)const fibonacci = (n) => { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2);};
const App = () => { const [num, setNum] = useState(10); // User input number const [count, setCount] = useState(0); // Other unrelated state
// Use useMemo to cache calculation results const fibValueMemo = useMemo(() => { console.log("Calculating Fibonacci..."); return fibonacci(num); }, [num]);
// Without caching // const fibValue = fibonacci(num)
return ( <div style={{ padding: "20px" }}> <h1>useMemo CPU-Intensive Operation Caching Example</h1> <div> <label> Calculate the{" "} <input type="number" value={num} onChange={(e) => setNum(parseInt(e.target.value, 10) || 0)} style={{ width: "50px" }} />{" "} th Fibonacci number: </label> </div> <p>Result: {fibValueMemo}</p> <button onClick={() => setCount(count + 1)}>Click count: {count}</button> </div> );};
export default App;
When the fibonacci
parameter reaches above 38
, the UI will start to stutter. Without useMemo
, every button click will make the webpage feel unresponsive.
useCallback
is used for function caching, with the sole purpose of keeping its reference unchanged.
// ...const createOptions = useCallback(() => { return { serverUrl: "https://localhost:1234", roomId: roomId, };}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => { const options = createOptions(); const connection = createConnection(options); connection.connect(); return () => connection.disconnect();}, [createOptions]); // ✅ Only changes when createOptions changes// ...
When functions become dependencies, they must be cached first. Without caching, due to the nature of functional components, each run creates a brand new function, making dependency change detection always return true.
Don’t think that function declarations are expensive and caching saves this overhead. In fact, the example above is equivalent to:
const fn = () => { return { serverUrl: "https://localhost:1234", roomId: roomId, };};const createOptions = useCallback(fn, [roomId]);
The declaration of fn
absolutely cannot be avoided. useCallback
simply returns the previous function declaration when dependencies haven’t changed—don’t be fooled into thinking the declaration is skipped this time 😂.
Of course, if your function doesn’t use any state
or props
, it’s best to define it outside the component, which truly saves overhead.
Since we can cache functions and execution results, why not think about caching possibilities from a higher dimension and cache entire components?
Based on the characteristic that render functions are pure functions UI = f(state)
, as long as state
doesn’t change, this component’s execution result can be safely reused. React provides the React.memo
function to store components, eliminating re-renders when a component receives the same parameters.
const ExpensiveItem = React.memo(({ count }) => { blockMainThread(500); const arr = Array(count).fill(0); return ( <> {arr.map((i) => ( <div>i</div> ))} </> );});
Scheduling
The following two optimizations are closely related to the new rendering mechanism:
useTransition
in a nutshell: rendering for transitions. Renders triggered within startTransition
can be immediately interrupted to execute higher-priority needs.
This Hook essentially leverages Fiber’s interruptible mechanism, directly interrupting current unimportant renders to give users the content they need next.
Check out this Demo:
export default function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState("about");
function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); }
return ( <> <Suspense fallback={<div>Loading, please wait...</div>}> <DelayedComponent /> </Suspense> <TabButton isActive={tab === "about"} onClick={() => selectTab("about")}> About </TabButton> <TabButton isActive={tab === "posts"} onClick={() => selectTab("posts")}> Posts (slow) {isPending ? "loading" : ""} </TabButton> <TabButton isActive={tab === "contact"} onClick={() => selectTab("contact")}> Contact </TabButton> <hr /> {tab === "about" && <AboutTab />} {tab === "posts" && <PostsTab />} {tab === "contact" && <ContactTab />} </> );}
You can feel the dramatic improvement in responsiveness by commenting out startTransition
.
Another Hook, useDeferredValue
, in a nutshell: React’s specialized advanced debouncing. useDeferredValue
is scheduled by React—it gets prioritized when the thread isn’t busy and deferred when it is.
When React detects the system is busy, it won’t trigger useDeferredValue
, so this value only updates after the busy period ends. It works wonderfully when combined with React.memo
.
Check out this Demo:
import React, { useState, useDeferredValue, startTransition } from "react";
function blockMainThread(duration) { const start = performance.now(); while (performance.now() - start < duration) {}}
const ExpensiveItem = React.memo(({ count }) => { blockMainThread(500); const arr = Array(count).fill(0); return ( <> {arr.map((i) => ( <div>i</div> ))} </> );});
const App = () => { const [count, setCount] = useState(0); const deferredCouont = useDeferredValue(count); console.log(count, deferredCouont); return ( <> <button onClick={() => setCount(count + 1)}> <span role="img" aria-label="react-emoji"> ⚛️ </span>{" "} {count} </button> <ExpensiveItem count={deferredCouont} /> </> );};
export default App;
The Special Case of Breaking Pure Functions
As pure function rendering components sometimes inevitably need to produce side effects, for example, after rendering you might need to automatically request AJAX interfaces to provide data for the page. React introduces the useEffect
Hook, where you can safely place your side effects.
Common side effect operations include:
- Data fetching (such as calling APIs)
- Event subscriptions (such as WebSocket or DOM event listeners)
- Manual DOM manipulation (such as directly operating on document)
- Timer setup (such as setTimeout or setInterval)
- Modifying global variables or external state
Taking Responsibility for Side Effects
If side effects are introduced when a component mounts, we may have the responsibility to clean up these side effects when the component unmounts.
useEffect(() => { const connection = createConnection(serverUrl, roomId); return () => connection.disconnect();}, [roomId]);
You may have heard about the issue of useEffect
running twice—this is a popular problem caused by React’s recommended strict mode in development environments. Strict mode intentionally runs twice to expose whether useEffect
side effects are properly cleaned up. Normally, pure functions should produce the same results, so repeated execution shouldn’t cause any problems. If problems occur, it means developers haven’t followed React’s requirement that rendering must be pure functions, or that side effects brought by useEffect
haven’t been cleaned up, causing the program to behave unexpectedly.
If you immediately notice abnormalities upon opening, it’s likely caused by strict mode’s repeated execution. What we should do at this point is not to disable strict mode, but to consider whether the component has introduced unexpected side effects.
Events and Side Effects
Besides useEffect
, events can also be non-pure functions because events don’t run during rendering. The difference between events and useEffect
is: events are caused by user actions, while side effects triggered by useEffect
are strongly associated with React rendering.
useEffect
is a function that has the opportunity to trigger whenever a component renders, whether it triggers depends on whether its dependency values have changed.
The official documentation dedicates considerable space to explaining situations where beginners might use useEffect
but don’t actually need it. One important concept is event priority. Here’s a simple example:
useEffect(() => { setSearch("");}, [currentTeam]);
When search
doesn’t depend on currentTeam
, this code will trigger the warning This hook specifies more dependencies than necessary: currentTeam
(using Biome). This approach is indeed fabricating dependency relationships. A better handling method is to reset search in the event that triggers currentTeam
itself.
Besides making these fabricated relationships difficult to understand during later code maintenance, this also affects application performance. When currentTeam
changes, it already triggers a re-render, and then calling setSearch
after re-rendering requires running render again. A better solution is as follows:
<button onClick={() => { setCurrentTeam(item.teamId); setSearch(""); refetch(); }}> {item.name}</button>
In this scenario, useEffect
is somewhat similar to Vue’s watch
. By the same logic, Vue shouldn’t abuse watch
either.
To summarize: make good use of event-driven approaches. For problems that can be solved with events, don’t use useEffect
. (This applies to Vue as well)
Even if you must use useEffect
, React has additional requirements: don’t write dependencies carelessly.
Events in Side Effects
Let’s look at another example: suppose a component needs to reconnect websocket when roomId
changes, but the websocket connection function also needs to use theme
. If we add theme
to dependencies, it will unnecessarily reconnect when changing themes.
React introduces a new Hook useEffectEvent
specifically for useEffect
dependency issues. Developers can use this Hook to abstract functions into “side effect events” that are dedicated to useEffect
.
It has several noteworthy points:
- Not a “reactive value” and cannot become a dependency
- No dependencies, but always maintains the latest referenced values
- From its name, it should only be used with
useEffect
This is undoubtedly a Hook that’s very difficult for developers to understand, since it’s specifically designed to solve useEffect
dependency problems—it’s somewhat like digging another hole to fill one hole.
If we must explain it, we can roughly understand it as: “events” (or even functions) were never meant to be triggers for changes—state is. But now React treats all functions as reactive values, which is a problematic design.
// beforefunction ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on("connected", () => { showNotification("Connected!", theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]);
return <h1>Welcome to the {roomId} room!</h1>;}
// afterfunction ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification("Connected!", theme); });
useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on("connected", () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>;}
That said, useEffectEvent
is still in experimental stage, so this problem currently needs to be handled by ignoring linter warnings.

useEffect is your last resort
要记住,useEffect
是最后杀招,可以用事件,优先用事件!实在没有事件触发时才会考虑使用它。
React’s Future
JSX based on functional components is indeed becoming increasingly flexible, but the cost is a steep learning curve. Whether it’s understanding the various concepts of functional components, handling performance issues, or dealing with useEffect
dependency problems, all these designs create mental burden for beginners.
Fortunately, the React team has recognized this problem. In future versions, forwardRef
will be removed, and React compiler’s automatic caching mechanism (including fine-grained caching of component content and function execution result caching)—many of the optimization content mentioned above—will no longer require additional developer control.
From the initial Fiber for rendering performance optimization, to changing the component paradigm to functional components better suited for Fiber, to later introducing Hooks that let Fiber squeeze out performance, this evolution has indeed made things progressively better.
There should be more Hooks in the future to perfect React’s design philosophy. The functional approach will likely continue all the way—early adoption brings early benefits, late adoption brings ease.