Logic:
I have a dialog for converting units. It has two stages of choice for the user: units to convert from and units to convert to. I keep this stage as a state, dialogStage, for maintainability as I'm likely going to need to reference what stage the dialog is in for more features in the future. Right now it's being used to determine what action to take based on what unit is clicked.
I also have a state, dialogUnits, that causes the component to rerender when it's updated. It's an array of JSX elements and it's updated via either foundUnitsArray or convertToUnitsArray, depending on what stage the dialog is at. Currently both states, dialogStage and dialogUnits, are updated at the same moment the problem occurs.
Problem:
When choosing the convertTo units, displayConversionTo() was still being called, as though dialogStage was still set to 'initial' rather than 'concertTo'. Some debugging led to confusion as to why the if (dialogStage == 'initial') was true when I'd set the state to 'convertTo'.
I believe that my problem was that the dialogStage state wasn't updated in time when handleUnitClick() was called as it's asynchronous. So I set up a new useEffect that's only called when dialogStage is updated.
The problem now is that the dialog shows no 'convertTo' units after the initial selection. I believe it's now because dialogUnits hasn't updated in time? I've swapped my original problem from one state not being ready to another state not being ready.
Question
How do I wait until both states are updated before continuing to call a function here (e.g. handleUnitClick()?).
Or have I mistaken what the problem is?
I'm new to react and, so far, I'm only familiar with the practice of state updates automatically rerendering a component when ready, unless overridden. Updating dialogUnits was displaying new units in the dialog until I tried to update it only when dialogStage was ready. It feels like an either/or situation right now (in terms of waiting for states to be updated) and it's quite possible I've overlooked something more obvious, as it doesn't seem to fit to be listening for state updates when so much of ReactJs is built around that already being catered for with rerenders, etc.
Component code:
function DialogConvert(props) {
const units = props.pageUnits;
const [dialogUnits, setDialogUnits] = useState([]);
const [dialogStage, setDialogStage] = useState('initial');
let foundUnitsArray = [];
let convertToUnitsArray = [];
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
useEffect(() => {
setDialogUnits(foundUnitsArray);
}, []);
useEffect(() => {
if (dialogStage == "convertTo") {
setDialogUnits(convertToUnitsArray);
}
}, [dialogStage]);
function handleClickClose(event) {
setDialogStage('initial');
props.callbackFunction("none");
}
function handleUnitClick(homogName) {
if (dialogStage == "initial") {
// getConversionChoices is an external function that returns an array. This returns fine and as expected
const choices = getConversionChoices(homogName);
displayConversionTo(choices);
} else if (dialogStage == "convertTo") {
// Can't get this far
// Will call a function not displayed here once it works
}
}
function displayConversionTo(choices) {
let canConvertTo = choices[0]["canconvertto"];
if (canConvertTo.length > 0) {
canConvertTo.forEach(element => {
convertToUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
setDialogStage('convertTo');
}
}
return (
<React.Fragment>
<div className="dialog dialog__convertunits" style={divStyle}>
<h2 className="dialogheader">Convert Which unit?</h2>
<div className='js-dialogspace-convertunits'>
<ul className="list list__convertunits">
{dialogUnits}
</ul>
</div>
<button className='button button__under js-close-dialog' onClick={handleClickClose}>Close</button>
</div>
</React.Fragment>
)
}
So, there are some issues with your implementations:
Using non-state variables to update the state in your useEffect:
Explanation:
In displayConversionTo when you run the loop to push elements in convertToUnitsArray, and then set the state dialogStage to convertTo, you should be facing the issue that the updated values are not being rendered, as the change in state triggers a re-render and the convertToUnitsArray is reset to an empty array because of the line:
let convertToUnitsArray = [];
thus when your useEffect runs that is supposed to update the
dialogUnits to convertToUnitsArray, it should actually set the dialogueUnits to an empty array, thus in any case the updated units should not be visible on click of the initial units list.
useEffect(() => {
if (dialogStage == "convertTo") {
// as your convertToUnitsArray is an empty array
// your dialogue units should be set to an empty array.
setDialogUnits(convertToUnitsArray)
}
}, [dalogStage]);
You are trying to store an array of react components in the state which is not advisable:
http://web.archive.org/web/20150419023006/http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#what-components-should-have-state
Also, refer https://stackoverflow.com/a/53976730/10844020
Solution: What you can do is try to save your data in a state, and then render the components using that state,
I have created a code sandbox example how this should look for your application.
I have also made some changes for this example to work correctly.
In your code , since you are passing units as props from parent, can you also pass the foundUnitsArray calculated from parent itself.
setDialogUnits(props.foundUnitsArray);
and remove the below operation,
units.unitsFound.forEach(element => {
foundUnitsArray.push(<DialogGroupChoice homogName={element} pcbOnClick={handleUnitClick} />);
});
Related
One thing I've recently been confused about is what the best way to run effects after a useState call, when the useState doesn't necessarily change the previous state value. For example, say I have a submit function in a form. Also say I have a state called randomNumber, and say in that submit function I call setRandomNumber and set the random number state to a random number from 1-2. Now say that for some reason every time the randomNumber is set, regardless of whether its value changes or not, we want to update the number in some database and navigate to a different page. If I use a useEffect(() => {updateDatabase(); }, [randomNumber]), the problem is that this will update the database even when the functional component first renders, which I do not want. Also, if the randomNumber doesn't change because we pick the same random number twice, we won't update the database after the second setRandomNumber call and we won't navigate to a different page which I don't want.
What would be the best way to 'await' a useState call. I read some articles saying we should potentially use flushSync, but I tried this and it doesn't seem to be working and it seems like this is a very unstable solution as of now.
Thanks!!!
You have 2 different requests here.
How to skip a useEffect on the first render. For this you could employ useRef as this does not trigger a re-render
const Comp = () => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return
} else {
// do your stuff
}
}, [randomNumber])
}
How to trigger useEffect even if you get the same number. Probably the simplest way would be to wrap your number inside an object and always generate a new object when generating a new number. This way you'll get a new ref all the time and the useEffect will always trigger. If you already have an object you could create a copy using spread operator. Something like this:
const Comp = () => {
const [randomNumber, setRandomNumber] = useState({number: 0})
useEffect(() => {
console.log(randomNumber.number)
}, [randomNumber])
// generate a new random number and set it like this
setRandomNumber({number: Math.random() + 1))
}
I came from here so please don't mark it as a duplicate.
I'm trying to get every value of mainState to be placed in anotherState so at the end I have an array of arrays. mainState is an changes on hover and it changes a lot so I can't assign every value I have in it to anotherState, I only need the value when clicked.
const [mainState, setMainState] = useState([])
const [anotherState, setAnotherState] = useState([])
const [currentItem, setCurrentItem] = useState(0)
//mainState changes on hover and it can change a lot
const onClickHandler = () => {
setAnotherState([...anotherState, mainState])
if (currentItem >= 4) {
// done with this component move to the next component
} else {
setCurrentItem(currentItem + 1)
setMainState([])
}
}
return (
<div onClick={onClickHandler}></div>
However, this doesn't work and even though mainState updates perfectly everytime, anotherState is always one step behind. currentItem represents an array index so I can't click one more time to get the last value of mainState in anotherState. I , using useEffect() which broke React because hover changes a lot and I ended up with an array of hundreds of values.
// tried useEffect() like in the question above, ended up with an array of hundreds of values
useEffect(() => setAnotherState([...anotherState, mainState]))
// tried using callback function like in one of the comments which didn't work and remained the same
setAnotherState(() => [...anotherState, mainState])
// another different way not using hooks is I created an empty array constant in the component
// and on click I tried pushing the value of mainState to that array using deep copy
// I keep ending up with the last array pushed only
const arrays = []
const onClickHandler = () => {
arrays.push(JSON.parse(JSON.stringify(mainState)))
}
How can I work around this?
setAnotherState is asynchronous. So if you want to get latest update value of anotherState.
You should use useEffect like
useEffect(() =>
Console.log("anotherState",anotherState)
// Do your stuff
,[anotherState])
This hook will called whenever another state gets updated using setAnotherState.
I have a paginator that determines how many pages to present, based off a data prop that is JSON array's of data from an API.
const itemsPerPage = 3;
const [noOfPages] = React.useState(
Math.ceil(data[0]?.length / itemsPerPage)
);
My issue is, how can I create some sort of conditional to wait for the noOfPages state to fire off before receiving the data prop?
Without ?., I'm getting an undefined issue.
{ data && console.log(data[0].length)} This works just fine, when testing if there is actually data inside the prop. I am not able to re-create this same sort of conditional for my state to wait for the data prop before firing off.
React always renders current state of your application. This means that all state variables exist as long as component that said values attached to exist. Your goal is to orchestrate said state to render what you want to render - that is called "state management".
You can go with different solutions here. For example, you can avoid rendering component until data is loaded, or replace that component with some sort of indication. Or you can set your state to some initial value, like 0 and render null until it will become some positive number. This will not work if it is possible that your value will be 0, but this can be mitigated with another state value. Consider this example:
function Pagination() {
// Here we have one of two possible distinct states: "loading" and "not_loading"
// It is good to use strings for such states, since later you might want to introduce new state, like "error"
let [loading_state, set_loading_state] = useState("loading");
// This is number of pages. It should always exist, but until we are in "not_loading" state we don't actualy care what it would be
let [number_of_pages, set_number_of_pages] = useState(0);
useEffect(function () {
// retrieve data
let data = ...;
// set number of pages and loading state
set_number_of_pages(data[0].length / items_per_page);
set_loading_state("not_loading");
}, []);
if (loading_state === "loading") return null;
if (loading_state === "not_loading") return ....;
}
There would be modifications, depending on where you keep and how you retrieve your data. Sometimes it is even better to calculate values "on the fly", instead of keeping them in state. If there would be some performance heavy computation, you can always use useMemo hook, but this is not your case.
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.
I am trying to change a variable in react with a callback function but cannot seem to do so. Here is my react component:
const MyComponent = () => {
let scenePinned;
const sceneCallback = event => {
if (event && event.state === 'DURING') {
console.log('Pinned');
scenePinned = true;
} else {
console.log('Not Pinned');
scenePinned = false;
}
};
console.log(scenePinned);
return (
<div>
<div style={scenePinned ? 'pinned' : 'not pinned'}/>
{(progress, event) => (
//Stuff Happens Here
), sceneCallback(event) )}
</div>
);
}
I am using react-scrollmagic and am trying to get the scenePinned variable to change from false to true and back to false again when scene is pinned to top. The console logging of Pinned and Not Pinned is happening correctly but I cannot seem to change the scenePinned variable. I am sure this is something very basic that I am not getting but I cannot understand why this is happening. Any help would be appreciated.
Note: I have tried using state to store the value but the callback is fired on scroll so the maximum depth is exceeded when trying to use state to store the scrolling status.
You need to use state for this. Otherwise the variable is reinitialized every time the component is rendered, and the value is lost.
console.log(scenePinned);
will run for the first time when the page loads
with react we use state the handle dynamic values.
or use rxjs
or create your own object and set listeners on it. with some custom event
so ex. with state
state={scenePinned:null}
then inside render method console.log(this.state.scenePinned)
A possible solution is to define a state variable in a parent component that will pass it to <MyComponent> as a prop.
Them move the sceneCallback function to the parent component and pass it as a prop to <MyComponent>
An explanation on how to define such a callback exists in many places. Here is one: (mine... ;) https://stackoverflow.com/a/55555578/5532513