How does react updates counter variable when used in useState hook

const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter + 1);

return <button onClick={() => {
increment();
increment();
}>{counter}</button>
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter + 1);

return <button onClick={() => {
increment();
increment();
}>{counter}</button>
Can someone explain why this implementation only increments the button once, even though increment was called twice? Also, notice that counter is a const, right? How come this value is updated each time we click our component? I'm a bit confused. What is happening under the hood?
10 Replies
prophile
prophile4w ago
(1) It's constant throughout each execution of the function. The first time it's rendered it's a constant 0, the second time a constant 1, etc. const means it can't be set to something else later on in that scope, not that it's the same every time the function is called
Faker
FakerOP4w ago
yeah I see, when the component is first rendered, React gives it a state of 0, each time we call the function useState() has the current state assigned to counter?
prophile
prophile4w ago
(2) increment binds the value of counter when it's created, and that's 0. It's a function which sets the counter to that original value + 1, that is, increment is setCounter(1) and you're calling it twice. Then when the component is re-rendered it'll be rebound to being effectively setCounter(2) some of this stuff ends up being pretty non-obvious in React unfortunately The usual way to deal with this is something like:
const increment = () => setCounter((count) => count + 1);
const increment = () => setCounter((count) => count + 1);
which will make it behave like you expect it to
Faker
FakerOP4w ago
one question though we say counter is a const but when the function increment occurs, how can it modify counter?
ᴋʙ
ᴋʙ4w ago
const Component = ({}) => {/* start */


/* end */
return <button>...<button>
};
const Component = ({}) => {/* start */


/* end */
return <button>...<button>
};
Everything between start and end is code that runs every render. setCounter triggers a re-render. This means that the const is not modified because it is a different const. Equivalent example:
const render1 = () => {
const x = 1;
console.log(x); // 1
};

const render2 = () => {
const x = 2;
console.log(x); // 2
};
const render1 = () => {
const x = 1;
console.log(x); // 1
};

const render2 = () => {
const x = 2;
console.log(x); // 2
};
Also this is more accurate:
const increment = () => setCounter(prevCount => prevCount + 1);
const increment = () => setCounter(prevCount => prevCount + 1);
Also if you are wondering how the values can be different. It is because useState saves its state outside of the rendering cycle.
Faker
FakerOP4w ago
one thing I don't understand though When we pass in an arrow function as argument in the setCounter() function, how is that different from passing counter itself, I mean, what's special about the arrow function in this case?
ᴋʙ
ᴋʙ4w ago
Its called a callback. its when you send a function as an argument/parameter. example:
// Step 3: arg recieves 123 and logs it.
const arg = num => console.log(num); // 123

// Step 2: example recieves num => console.log(num);
const example = param => param(123); // Calls arg with 123

// Step 1: call example with arrow function aka callback.
example(arg);
// Step 3: arg recieves 123 and logs it.
const arg = num => console.log(num); // 123

// Step 2: example recieves num => console.log(num);
const example = param => param(123); // Calls arg with 123

// Step 1: call example with arrow function aka callback.
example(arg);
curiousmissfox
UseState is a function that returns an array. So just like you can use const for an array; const   only prevents reassignment of the variable identifier itself. The array reference remains constant, but the elements inside the array can be mutated (like with arr.push() )
ᴋʙ
ᴋʙ4w ago
This is basically what is happening behind the scenes simplified to javascript:
let state = {};
const useState = initialState => {
// Helpers
const isInitialized = key => Object.hasOwn(state, key);
const isFunction = value => value instanceof Function;

// "Private" update function to set current state.
const updateState = prevState => {
if (!isFunction) state.current = prevState;
else state.current = prevState(state.current);
};

// Runs only once on first useState call.
if (!isInitialized('initial')) {
state.initial = initialState;
state.current = initialState;
}

// Returns current state and private update function.
return [state.current, updateState];
};


// Remember code within {} scope runs once every render.

// IMPORTANT: React automatically batches/merges all state updates, regardless of where they're called within scope.
const Component ({}) => {

// [0, prevState => {...}]
const [count, setCount] = useState(0) // count starts at 0

// count is 0, queues update to: 0 + 1 = 1
setCount(count + 1);

// count is still 0, queues update to: 0 + 1 = 1
setCount(count + 1);

// count is still 0, queues update to: 0 + 1 = 1
setCount(count + 1);

// Takes current queued value (1), adds 10 = 11
setCount(prevCount => prevCount + 10);

// Takes previous result (11), adds 10 = 21
setCount(prevCount => prevCount + 10);

// Takes previous result (21), adds 10 = 31
setCount(prevCount => prevCount + 10);

// Final count number = 31;

return <JSX>
}
let state = {};
const useState = initialState => {
// Helpers
const isInitialized = key => Object.hasOwn(state, key);
const isFunction = value => value instanceof Function;

// "Private" update function to set current state.
const updateState = prevState => {
if (!isFunction) state.current = prevState;
else state.current = prevState(state.current);
};

// Runs only once on first useState call.
if (!isInitialized('initial')) {
state.initial = initialState;
state.current = initialState;
}

// Returns current state and private update function.
return [state.current, updateState];
};


// Remember code within {} scope runs once every render.

// IMPORTANT: React automatically batches/merges all state updates, regardless of where they're called within scope.
const Component ({}) => {

// [0, prevState => {...}]
const [count, setCount] = useState(0) // count starts at 0

// count is 0, queues update to: 0 + 1 = 1
setCount(count + 1);

// count is still 0, queues update to: 0 + 1 = 1
setCount(count + 1);

// count is still 0, queues update to: 0 + 1 = 1
setCount(count + 1);

// Takes current queued value (1), adds 10 = 11
setCount(prevCount => prevCount + 10);

// Takes previous result (11), adds 10 = 21
setCount(prevCount => prevCount + 10);

// Takes previous result (21), adds 10 = 31
setCount(prevCount => prevCount + 10);

// Final count number = 31;

return <JSX>
}
Faker
FakerOP4w ago
yep I see, thanks !

Did you find this page helpful?