function App() {
const [count, setCount] = useState(0);
function increase() {
setCount(count + 1);
console.log(count);
}
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>+</button>
</div>
);
}
When I console.log the value of count, it increases by 1 even though I have declared it using const. How is it still being updated? I have a feeling it's related to the useState function, but I'm not sure.
Another thing is, if we can update using count + 1, why can't we do so using count++?
Thanks a lot for the guidance!
Code is here: https://codesandbox.io/s/g8dgv0?file=/src/components/App.jsx
React is fundamentally a rendering library, and the paradigm it uses is that the info that's rendered on the screen should flow directly from the current state stored in React. So, calling a state setter does two things:
It updates the internal state React has for that state value
It tells the component to re-render, so as to generate a new view based on the new updated state
Whenever a functional component re-renders, the function runs again - and useState can return different values depending on what the current value of the internal state is.
For a similar example in vanilla JS:
let internalValue = false;
const getValue = () => internalValue;
const setValue = (newValue) => {
internalValue = newValue;
setTimeout(App);
};
const App = () => {
const stateValue = getValue();
if (!stateValue) {
setValue(true);
}
console.log('app rendered with stateValue of', stateValue);
};
App();
Above, you can do const stateValue = getValue(); and get different values for stateValue depending on other factors - because the App function ran again.
Another thing is, if we can update using count + 1, why can't we do so using count++?
Because, as explained in the beginning of the answer, the view should flow from the state in React, and when updating the state, you need to tell React to re-render; reassigning a variable, by itself, doesn't have side effects. You need to call the state setter to update React's internal state and then re-render with the new state.
So, you need
setCount(count + 1);
and not
count++;
because only the former will result in a re-render (even if you had declared count with let).
The '0' value in your useState(0) is the default/initial value that the state will start with. From there you can modify it using setCount(value)
For your second question, from howtojs.io:
In React, we should not modify the state variables directly. We need
to pass the new data to the state variable modifier functions (such as
setCount), then React will modify the value of the state variable.
One more thing we need to remember is, when we do count++ in
JavaScript, the variable will be incremented after consuming the
variable first.
So, when we do setState(count++), first the value of count will be
passed to the setState function, then the value of count will be
incremented inside the function component (this is not recommended
way).
When I console.log the value of count, it increases by 1 even though I
have declared it using const.
When you are console logging the count state you are actually logging the unupdated current state value. In other words, nothing has changed yet. It's only at the end of the render cycle when enqueued state updates are processed that the component rerenders (i.e. calls the function body) with the updated state value.
How is it still being updated? I have a feeling it's related to the
useState function, but I'm not sure.
Yes, the useState hook returns the current state value and a function to call to enqueue an update to state.
const [count, setCount] = useState(0);
In the increase callback handler the setCount function is called with the next value you are requesting React to update the state to.
function increase() {
setCount(count + 1);
}
As described above, the update is enqueued and at then end of the render cycle is processed by React and triggers the component to rerender.
Another thing is, if we can update using count + 1, why can't we do so
using count++?
In React we absolutely DO NOT mutate state. The count++, if it could work and not throw an error, would still be a state mutation. Trying to use count++ is the same as trying to do count = count + 1 which we just simply don't do in React.
In more general Javascript terms, trying to post-increment with count++ while count is declared const would throw an error. When a variable is declared const this only means it can't have a value reassigned to it after initialization. This doesn't mean that the value currently assigned is static/constant. If we had declared let [count, setCount] = useState(0) then by Javascript standards doing count++ is perfectly legal.
We use the React state updater functions to enqueue state updates and generally return new object/value references.
The hook remains immutable in the scope of the function. When you change it, the component re-renders.
Type const in JS doesn't prevent you from making operations on the variable. Adding or subtraction of values will be interpreted without any issues. This the count is increasing.
However count++ shorthand is written for count = count + 1 where a assignment operation is taking place and it is prohibited by the type const.
Related
I have been using these two ways interchangeably however I am not sure which one is the more correct. their behavior seems to be the same but I am sure there is a use case for each. Anyone can help me understand what's the proper use for each case?
const [customer, setCustomers] = useState(props.location.state);
useEffect(() => {
setCustomers(props.location.state);
}, []);
You should normally stick to the first one. Calling the setter of useState may lead to undesired re-renders and decreased performance.
In the first block the customer is initialised directly and no re-render happens. The setCustomer method will change the state and rerender the component. In the end the whole function will run twice which you can verify with a console.log.
const [customer, setCustomers] = useState(0);
useEffect(() => {
setCustomers(15);
}, []);
console.log(customer) // will first output 0 and then 15
Assuming in the second case, you have this as your useState statement:
const [customer, setCustomers] = useState();
The second one sets the value of customer on componentDidMount. So in the initial render, you will not have the appropriate value in your customer variable.
But yes, very soon after that the correct value will be set because of the code written in useEffect.
To clear it up, there will be 2 renders here (because the state variable value changes). In the first one, that won't be the case since the state variable has only one value from beginning.
The first one is more effective.
const [customer, setCustomers] = useState(props.location.state);
If you use second one (by using useEffect), your component will be re-rendered again.
That's, your state variable customer will be updated in useEffect after DOM is initially rendered, this leads the 2nd re-render of the component.
But if you want customer to be updated by props.location.state, you need to add useEffect hook like the following.
useEffect(()=> {
setCustomers(props.location.state);
}, [props.location.state]);
Setting the state's default value upon declaring it is probably the more correct way to go, since it does not trigger a re-render.
Every time you call a setState your component will be re-rendered, so when you do so in the useEffect, you will trigger an unnecessary re-render upon the component mounting, which could be avoided by doing the good ol'
const [value, setValue] = useState(props.location.state)
While of course there are exceptions and many different use cases, setting an initial state in a useEffect is more useful, for example, when you have values you'd expect to change regardless of your component (for example from an external asynchronous API call):
const [value, setValue] = useState(valueExpectedToChange)
useEffect(() => {
setValue(valueExpectedToChange) // will trigger the rerender only when valueExpectedToChange changes
}, [valueExpectedToChange])
I'm reading the React Documentation and there is a topic that I didn't understand very well:
https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
}
Demo:
https://codesandbox.io/s/get-previous-state-ref--hook-faq-w0f3k?file=/src/App.js
I'd like a better explanation about this. Kind, how it's works?. Thanks
There's three pieces here. A state, a ref, and an effect.
A state is a value that is remembered between renders, and it re-renders it's component when changed.
A reference (or ref) is a value that persists between renders like state does, but unlike state it's not watched for any changes and can be mutated freely. Changes to refs never cause renders.
An effect is simply a function that runs after a render, or after a render in one of its dependencies has changed since the previous render.
So putting that together here's what happens:
const [count, setCount] = useState(0);
Here a state is declared. Calling setCount with a new value will cause the component to render, and when it does, count will have the value that was set.
const prevCountRef = useRef();
Then declare the ref. This will hold a reference to the previous count value. Right now it no value (undefined), which makes sense considering there is no previous value when the component first renders.
useEffect(() => {
prevCountRef.current = count;
});
Now declare an effect which runs after every render (you can tell this because it has no dependencies (would be the second argument to useEffect()). When that runs it sets the current value of prevCountRef to whatever the count is.
So, on the first render, in pseudo code:
// render starts
count // 0
prevCountRef.current // undefined
// render finishes
effect runs {
prevCountRef.current is set to 0
}
So when the render is happening the count state is 0 and the previous count ref is undefined.
Second render:
setCount(1) // triggers the second render
// second render starts
count // 1
prevCount.current // 0
// second render finishes
effect runs {
prevCount.current is set to 1
}
So as you can see, during the render you always have the current count as as state, and the previous count as the ref. This works because the effect that sets the refs value always runs after the render is done, which allows it to be whatever the previous value was.
You can use componentDidUpdate to get the prev props and state. When you extend React.Component, it will call componentDidUpdate after each render. You define the function, and React will pass the prev props and prev state to you.
https://reactjs.org/docs/react-component.html#componentdidupdate
useEffect is called just after the component render.
The first time the component render prevCount is undefined because useEffect has not been called yet.
pre-render:
1) value: 0, prevCount: undefined
post-render:
useEffect is called:
2) value: 0, prevCount: 0 useRef don't cause a new rendering, thus the component is not updated. refCount will be shown as undefined.
when you change the count value the component gets re-render:
pre-render:
3) value: 1, prevCount: 0 useEffect is not called yet.
post-render:
4) value: 1, prevCount: 1 useEffect is called, prevCount increased but the component is not updated so prevCount will be shown the the value = 0;
I have a simple cart function that, when a user clicks to increase or decrease the quantity of an item in a shopping cart, calls a useState function to update the cart quantity in state.
const [cart, setCart] = useState([]);
const onUpdateItemQuantity = (cartItem, quantityChange) => {
const newCart = [...cart];
const shouldRemoveFromCart = quantityChange === -1 && cartItem.count === 1;
...
if (shouldRemoveFromCart) {
newCart.splice(cartIndex, 1);
} else {
...
}
setCart(newCart); //the useState function is called
}
so in jest, I have a function that tests when a user sets a cart item's quantity to zero, but it does not yet remove the item from the cart, I'm assuming because it has not yet received the results of setCart(newCart):
test('on decrement item from 1 to 0, remove from cart', () => {
const [cartItemToDecrement] = result.current.cartItems;
const productToDecrement = result.current.products.find(
p => p.id === cartItemToDecrement.id
);
act(() => {
result.current.decrementItem(cartItemToDecrement);
});
act(() => {
result.current.decrementItem(cartItemToDecrement);
});
...
expect(result.current.cartItems).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: cartItemToDecrement.id,
count: cartItemToDecrement.count - 2,
inventory: productToDecrement.inventory + 2
})
])
);
});
});
This test passes, because the cart now contains an item whose quantity has dropped to zero. But really it shouldn't, because from the splice operation in onUpdateItemQuantity our cartItems array should now not include the object at all. How do I verify in my jest test that this removal is happening (the react code works properly).
I do not really understand the relation between the test and your onUpdateItemQuantity very much because the context you provided is not sufficient to me.
But from your question, there are 2 clues which may help to save your time.
You may know setCart from useState is not synchronous, so that if you try to access cart from useState at the same frame, it shouldn't reflect the change even though you ensure setCart called. useEffect is a good solution. You can find the doc of useEffect [https://reactjs.org/docs/hooks-effect.html][1], any change should be reflected in useEffect, perhaps you can put your test there.
Add an extra variable myCart to store cart and a function setMyCart. Instead of calling setCart in your code, call setMyCart. setMyCart is like,
function setMyCart(newCart)
{
myCart = newCart;
setCart(myCart); // this is for triggering React re-render
}
then use myCart which can reflect the change immediately for testing.
The only purpose of the additional code in the 2nd point, is when we still rely on the re-render mechanism of React, we use our own Model myCart for our particular logic rather than cart state from React which is not only for View but also used for our logic on an inappropriate occasion.
Testing for state changes is a challenge, because it doesn't produce any specific output (except whatever impacts your render).
The key is to narrow the testing scope to your responsibilities, versus testing React. React is responsible for making sure that useState updates the state properly. Your responsibility is to make sure you are using the data properly and sending the correct data back. So instead of testing useState, test how your component responds to the state it is given, and make sure you are setting the correct state.
The answer to to mock useState. Give it an implementation that returns the initial value passed to it, with a mock function that lets you read what data is being saved.
More detailed answer here:
https://dev.to/theactualgivens/testing-react-hook-state-changes-2oga
HTH!
If you need to test every state update using useState(), you can do so by implementing a useEffect function that listens specifically for that state change, in your case, cart:
useEffect(() => {
test(...)
), [cart]);
And so you can do the test inside the useEffect() function.
That will ensure that you have the correct value of cart and it will be called every time cart is updated.
It's totally unclear how onUpdateItemQuantity is called. And even what it does exactly. Never the less, you claim that toEqual(...[{...,count:cartItemToDecrement.count - 2,...}]...) passes. That probably means that onUpdateItemQuantity is called inside of decrementItem, inside of onUpdateItemQuantity there is something like newCart[cartIndex].count--, and if toEqual() passes, that means that setState() successfully updates the state, and after act() you have the actualized state. And if test's name is correct and initial value of cartItemToDecrement.count === 1 that should mean that cartItemToDecrement.count - 2 === -1 and you never go inside of if (shouldRemoveFromCart). So maybe the issue is not with the test, but with the code.
Lets say I have the following component using react hooks
const Component = (props) => {
[state1, setState1] = useState();
[state2, setState2] = useState();
useEffect(()=>{
// use state1 and state2 as param to fetch data
...
}, [state1, state2])
onSomethingChange = () => {
setState1(...);
setState2(...);
}
return (..);
}
When onSomethingChange triggers, I thought it would call the side effect function twice since I update 2 different state vars in the same dependency array. And I was going to refactor those 2 vars into 1 object but I thought I would test them as 2 separate vars first.
What I observed is the side effect function only gets executed once even when I update 2 different state vars in the same dependency array, this is what I wanted but I don't know why. Could someone please explain how it works under the bonnet?
That's because React sometimes may batch multiple state changes into one update for performance. React will batch state updates if they're triggered from within a React-based event, that's the reason why your side effect block is called only once. For more information, you may refer this thread: https://github.com/facebook/react/issues/14259
React batches state updates under the hood.
That simply means that calling
setState1(...);
setState2(...);
in the same synchronous (!) execution cycle (e.g. in the same function) will NOT trigger two component re-render cycles.
Instead, the component will only re-render once and both state updates will be applied simultaneously.
Not directly related, but also sometimes misunderstood, is when the new state value is available.
Consider this code:
console.log(name); // prints name state, e.g. 'Doe'
setName('John');
console.log(name); // ??? what gets printed? 'John'?
You could think that accessing the name state after setName('John');
should yield the new value (e.g. 'John') but this is NOT the case.
The new state value is only available in the next component render cycle (which gets scheduled by calling setName()).
React docs state: don’t call Hooks inside loops, conditions, or nested functions.
Does calling a hook means just calling useState e.g. const [state, useState] = useState(0)?
What about calling setter in conditionals ?
Is this code breaking rules of hooks ?
const [oneHook, setOneHook] = useState(0)
const [anotherHook, setAnotherHook] = useState(false)
if (something) {
setOneHook(1)
setAnotherHook(true)
} else {
setOneHook(0);
setAnotherHook(false)
}
Thanks !
No, that code example does not break the rules of hooks. Every time the component renders there will be exactly the same number of calls to useState in exactly the same order, so that will be fine.
I do want to point out that setting state right away in the body of the component doesn't make much sense. When the component mounts it will start rendering with the initial values from state, but then before it can finish rendering the state has already changed and it has to start over. But presumably that's just an artifact of the example, and in your real code the if would be inside a useEffect or some other practical code location.
React docs state: don’t call Hooks inside loops, conditions, or nested functions.
Alright,the following code shows the example for the above statement. I had the same issue where i needed to set the state inside of a loop, like following
const [someArray, setArray] = useState([]);
someValue.forEach((value, index) => {
setArray([...someArray, value]) //this should be avoided
});
Above thing i have achieved like this following
var temp = [];
var counter = 0;
someValue.forEach((value, index) => {
temp.push(value);
counter++;
if (counter === someValue.length) {
setArray(temp)
}
});
if you are setting a state inside the loop than each time the component re renders which you do not want to get into.
Is this code breaking rules of hooks
No Your code looks fine, as you are setting up the state only based on condition for only once when the component renders