How callIndex works in react's useState under the hood?

Viewed 40

I'm currently learning how React works under the hood and built a custom useState from scratch following the tutorials on Youtube. But I'm having trouble understanding how callIndex is incremented and why do I need to set currentIndex variables which basically take the callIndex value.

The code works like this -

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";

let callIndex = -1;
let stateValues = [];
const useState = (initValue) => {
  callIndex++;
  console.log("callIndex", callIndex);
  const currentIdx = Number(callIndex);
  if (stateValues[currentIdx] === undefined) {
    stateValues[currentIdx] = initValue;
  }
  const setState = (newValue) => {
    console.log("currentIdx in setState", currentIdx);
    stateValues[callIndex] = newValue;
    render();
  };

  return [stateValues[currentIdx], setState];
};

const App = () => {
  const [countA, setCountA] = useState(1);
  const [countB, setCountB] = useState(-1);
  const [countC, setCountC] = useState(0);
  return (
    <div>
      <div>
        <h1>Count A: {countA}</h1>
        <button onClick={() => setCountA(countA - 1)}>Subtract</button>
        <button onClick={() => setCountA(countA + 1)}>Add</button>
      </div>
      <div>
        <h1>Count B: {countB}</h1>
        <button onClick={() => setCountB(countB - 1)}>Subtract</button>
        <button onClick={() => setCountB(countB + 1)}>Add</button>
      </div>
      <div>
        <h1>Count C: {countC}</h1>
        <button onClick={() => setCountC(countC - 1)}>Subtract</button>
        <button onClick={() => setCountC(countC + 1)}>Add</button>
      </div>
    </div>
  );
};
function render() {
  // callIndex = -1;
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById("root")
  );
}
render();

Every time the useState function is called, the callIndex variables would be incremented by 1. In this code, I called useState 3 times. So, the callIndex value would be 3.

The problem starts here - when I called the setState function, it'll first assign the newValue to the stateValues but in which index will it be changed.

Are there any articles or tutorials explaining how this happening?

1 Answers

The biggest issue is, you need to be able to distinguish between useState calls for different components, and different renders of the same component. You need this

<Comp1 />

const Comp1 = () => {
  const [countA, setCountA] = useState(1);
  const [countB, setCountB] = useState(-1);

to perform different logic than

<Comp1 />
<Comp2 />

const Comp1 = () => {
  const [countA, setCountA] = useState(1);

const Comp2 = () => {
  const [countB, setCountB] = useState(-1);

But the information available to you exposed by React with functional components is very limited, and won't allow you to do this easily. There is much more information used by (and available to) React's internals, which can distinguish things like the start of a component call.

If you want to "replicate" useState, I think the easiest way to do so would be to go outside of React completely, so that when "updating" the state, you can just call a function that re-populates a section of the DOM. (In React, you don't want to call ReactDOM.render on an app many times, and especially not on every render - that results in it being rebuilt from the ground up every time.)

Also, a small issue - when mounting, you should check if the state property exists on the array before assigning, not just if the value on that property is undefined, otherwise you'll lose the desired logic when nothing is passed as an initial value. This

  if (stateValues[currentIdx] === undefined) {
    stateValues[currentIdx] = initValue;
  }

should be

  if (!([currentIdx in stateValues)) {
    stateValues[currentIdx] = initValue;
  }

In your state setter, you also need to change

stateValues[callIndex] = newValue;

to

stateValues[currentIdx] = newValue;

For a vanilla JS demo of your tweaked code:

let callIndex = -1;
let stateValues = [];
const useState = (initValue) => {
  callIndex++;
  const currentIdx = Number(callIndex);
  if (!(currentIdx in stateValues)) {
    stateValues[currentIdx] = initValue;
  }
  const setState = (newValue) => {
    stateValues[currentIdx] = newValue;
    renderApp();
  };

  return [stateValues[currentIdx], setState];
};

const root = document.querySelector('.root');
const renderApp = () => {
  callIndex = -1;
  root.textContent = '';
  root.appendChild(App());
};
const App = () => {
  const [countA, setCountA] = useState(1);
  const [countB, setCountB] = useState(-1);
  const [countC, setCountC] = useState(0);
  const div = document.createElement('div');
  div.innerHTML = `
      <div>
        <h1>Count A: ${countA}</h1>
        <button>Subtract</button>
        <button>Add</button>
      </div>
      <div>
        <h1>Count B: ${countB}</h1>
        <button>Subtract</button>
        <button>Add</button>
      </div>
      <div>
        <h1>Count C: ${countC}</h1>
        <button>Subtract</button>
        <button>Add</button>
      </div>`;
   // this could be made less repetitive, but this is just for demo purposes
   const buttons = div.querySelectorAll('button');
   buttons[0].onclick = () => setCountA(countA - 1);
   buttons[1].onclick = () => setCountA(countA + 1);
   buttons[2].onclick = () => setCountB(countB - 1);
   buttons[3].onclick = () => setCountB(countB + 1);
   buttons[4].onclick = () => setCountC(countC - 1);
   buttons[5].onclick = () => setCountC(countC + 1);
   return div;
};
renderApp();
<div class="root"></div>

Related