Why Function Components Had No State Before React 16 and How Hooks Changed That
This article explains why React function components were stateless before version 16, how the introduction of Fiber and the useState hook gave them state, and dives into the internal mechanisms—including renderWithHooks, hook queues, and update scheduling—that make state updates work in modern React.
Why Function Components Had No State Before React 16?
Before React 16, function components could not hold their own state; any data had to be passed through
props. The only way to have mutable data was to use a class component with a
this.stateobject.
<code>const App = () => <span>123</span>;
class App1 extends React.Component {
constructor(props) {
super(props);
this.state = { a: 1 };
}
render() {
return (<p>312</p>);
}
}
</code>When the class component is compiled with Babel, it is transformed into a function component, but the resulting function still lacks a
rendermethod that stores state, so the component remains stateless.
<code>var App1 = /*#__PURE__*/function (_React$Component) {
_inherits(App1, _React$Component);
var _super = _createSuper(App1);
function App1(props) {
var _this;
_classCallCheck(this, App1);
_this = _super.call(this, props);
_this.state = { a: 1 };
return _this;
}
_createClass(App1, [{
key: "render",
value: function render() {
return /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { children: "312" });
}
}]);
return App1;
}(React.Component);
</code>The key difference between class and function components lies in whether the prototype contains a
rendermethod. During rendering React calls the class component's
rendermethod, while a function component’s "render" is the function itself; after execution its local variables are discarded, so on re‑render the previous state cannot be retrieved.
Why Do Function Components Have State After React 16?
React 16 introduced the Fiber architecture, which required a new data structure for each node (
fiber node). By adapting the component definition, function components can now store state inside the fiber.
<code>const App = () => {
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(1);
return <span>123</span>;
};
</code>How Does React Know Which Component a Hook’s State Belongs To?
All hook state is injected via
useState. During the render phase React calls a special function
renderWithHookswith six parameters:
current,
workInProgress,
Component,
props,
secondArg, and
nextRenderExpirationTime. Inside this function the variable
currentlyRenderingFiber$1records the fiber node that is being rendered.
<code>current: the node currently being rendered (null on first render)
workInProgress: the new node for the upcoming render
component: the component function or class
props: component props
secondArg: not used in this article
nextRenderExpirationTime: fiber render expiration time
</code>When
useStateruns, it reads
currentlyRenderingFiber$1to locate the correct fiber node and stores the hook’s state in that node’s
memoizedStatefield.
renderWithHooks is used only for function component rendering.
Why Is the State Structure Different Between Class and Function Components?
Class components keep state as a plain object on the instance, while function components store each hook’s state in a singly‑linked list attached to the fiber node. The list allows React to keep the order of hooks stable across renders.
<code>interface State {
memoizedState: any; // current hook state
baseState: any; // state before the current render
baseQueue: any; // pending updates from previous renders
next: State | null; // next hook in the list
queue: {
pending: any;
dispatch: any;
lastRenderedReducer: any;
lastRenderedState: any;
};
}
</code>What Happens When setA Is Called?
During the initial mount
useStatecreates a hook via
mountState, attaches a queue to the fiber, and returns the current state value together with a
dispatchfunction bound to that fiber.
<code>function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
</code>When the returned
dispatch(e.g.,
setA) is invoked, React creates an update object, pushes it onto the hook’s circular queue, and schedules work on the fiber.
<code>function dispatchAction(fiber, queue, action) {
var currentTime = requestCurrentTimeForUpdate();
var expirationTime = computeExpirationForFiber(currentTime, fiber, requestCurrentSuspenseConfig());
var update = {
expirationTime: expirationTime,
suspenseConfig: requestCurrentSuspenseConfig(),
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// enqueue the update in the circular linked list
var pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// mark the fiber for re‑render
scheduleWork(fiber, expirationTime);
}
</code>During the next render,
renderWithHooksdetects that the component is updating, swaps the dispatcher to
HooksDispatcherOnUpdateInDEV, and
useStatenow calls
updateStatewhich processes the queued actions, computes the new state, and stores it back into the fiber’s
memoizedState.
Why Is State Not Real‑Time When Using setTimeout ?
<code>const App3 = () => {
const [num, setNum] = React.useState(0);
const add = () => {
setTimeout(() => {
setNum(num + 1);
}, 1000);
};
return (<>
<div>{num}</div>
<button onClick={add}>add</button>
</>);
};
</code>The closure captures the stale
numvalue (0) at the time the timeout is created, so each delayed call adds 1 to the original value, resulting in only a single increment. Using the functional form
setNum(state => state + 1)lets React supply the latest state.
Why Can’t useState Be Called Inside Conditional Statements?
React relies on the order of hook calls to match the linked‑list of hook states. Placing a hook inside an
ifblock can change the order between renders, causing later hooks (e.g., C) to be skipped or mismatched, which leads to unpredictable state.
Why Are Hook States Stored in a Linked List?
The linked list guarantees a stable order of hooks across renders while allowing React to add or remove hooks without reallocating a fixed‑size array. This design supports the “pure function” model of function components.
Using a linked list lets function components manage state similarly to class components while keeping the component itself a pure function.
Conclusion
By reading the React source code, we can see exactly how
useStateis mounted, how updates are queued, and how the Fiber reconciler processes those updates. This deep understanding helps developers write more reliable hook‑based code and avoid common pitfalls.
The analysis above is based on React 16 and react‑dom 16.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.