Frontend Development 18 min read

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.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Why Function Components Had No State Before React 16 and How Hooks Changed That

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.state

object.

<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

render

method 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

render

method. During rendering React calls the class component's

render

method, 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.

Class vs Function component fiber nodes
Class vs Function component fiber nodes

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>
Fiber nodes of function vs class component
Fiber nodes of function vs class component

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

renderWithHooks

with six parameters:

current

,

workInProgress

,

Component

,

props

,

secondArg

, and

nextRenderExpirationTime

. Inside this function the variable

currentlyRenderingFiber$1

records 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

useState

runs, it reads

currentlyRenderingFiber$1

to locate the correct fiber node and stores the hook’s state in that node’s

memoizedState

field.

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

useState

creates a hook via

mountState

, attaches a queue to the fiber, and returns the current state value together with a

dispatch

function 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,

renderWithHooks

detects that the component is updating, swaps the dispatcher to

HooksDispatcherOnUpdateInDEV

, and

useState

now calls

updateState

which 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

num

value (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

if

block 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

useState

is 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.
Technical deep‑dive illustration
Technical deep‑dive illustration
State ManagementReactHooksFiberuseStateReact16function components
Tencent IMWeb Frontend Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.