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.

const App = () => <span>123</span>;

class App1 extends React.Component {
  constructor(props) {
    super(props);
    this.state = { a: 1 };
  }
  render() {
    return (<p>312</p>);
  }
}

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.

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);

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.

const App = () => {
  const [a, setA] = React.useState(0);
  const [b, setB] = React.useState(1);
  return <span>123</span>;
};
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.

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

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.

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;
  };
}

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.

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];
}

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.

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);
}

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 ?

const App3 = () => {
  const [num, setNum] = React.useState(0);
  const add = () => {
    setTimeout(() => {
      setNum(num + 1);
    }, 1000);
  };
  return (<>
    <div>{num}</div>
    <button onClick={add}>add</button>
  </>);
};

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
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.