React: infinite loop even though useEffect should detect change and prevent loop - javascript

const [label, setLabel] = useState([<>, <>]);
const [labelType, setLabelType] = useState('none');
useEffect( () => {
if (props.label === 'label1') {
setLabel([<div>...</div>, <div>...</div>])
}
if (props.label=== 'label2') {
setLabel([<div>...</div>, <div>...</div>]);
}
if (props.label === 'label3') {
setLabel([<div>...</div>, <div>...</div>])
}
}, [labelType] );
setLabelType(props.label)
return (
<div className=''>
<div id=''>{label}</div>
</div>
)
So I'm basically passing in a prop and just trying to use that value to decide what to set my {label} to and the initial value of props.label is 'label1'. If my console logs, I see that 'none' is changed to 'label1' by setLabelType(props.label) and my understanding is that since setLabelType() was used, the stated is updated/ component re-rendered and plus labelType specifically is changed then useEffect() is triggered and the appropriate branch/setLabel() is called based on props. Next render useEffect() is not called since 'label1' -> 'label1' is no change (vs. first time 'none' -> 'label1').
But reality is that I have an infinite loop instead, could anyone explain to me why? I think maybe I need to have setLabelType(props.label) in a function but the trigger for this code to execute is in a different component so there's no 'onChange' prop that would call it in this component (basically when sibling component is clicked, passes data to common parent, parent passes data back down to this child and use the prop to decide render). This seems really simple but I'm confused.
Thanks

Related

React setState of Parent component without rerendering the Child

I have a parent Component with a state variable that gets changed by one of its child components upon interaction. The parent then also contains some more components based on the data in the state variable.
The problem is that the child component rerenders when the state of its parent changes because the reference to the setState function changes. But when I use useCallback (as suggested here), the state of my parent just does not update at all.
This is my current setup:
function ArtistGraphContainer() {
const [artistPopUps, setArtistPopUps] = useState([])
const addArtistPopUp = useCallback(
(artistGeniusId, xPos, yPos) => {
setArtistPopUps([{artistGeniusId, xPos, yPos}].concat(artistPopUps))
},
[],
)
return (
<div className='artist-graph-container'>
<ArtistGraph addArtistPopUp={addArtistPopUp} key={1}></ArtistGraph>
{artistPopUps.map((popUp) => {
<ArtistPopUp
artistGeniusId={popUp.artistGeniusId}
xPos={popUp.xPos}
yPos={popUp.yPos}
></ArtistPopUp>
})}
</div>
)
}
And the Child Component:
function ArtistGraph({addArtistPopUp}) {
// querying data
if(records) {
// wrangling data
const events = {
doubleClick: function(event) {
handleNodeClick(event)
}
}
return (
<div className='artist-graph'>
<Graph
graph={graph}
options={options}
events={events}
key={uniqueId()}
>
</Graph>
</div>
)
}
else{
return(<CircularProgress></CircularProgress>)
}
}
function areEqual(prevProps, nextProps) {
return true
}
export default React.memo(ArtistGraph, areEqual)
In any other case the rerendering of the Child component wouldn't be such a problem but sadly it causes the Graph to redraw.
So how do I manage to update the state of my parent Component without the Graph being redrawn?
Thanks in advance!
A few things, the child may be rerendering, but it's not for your stated reason. setState functions are guaranteed in their identity, they don't change just because of a rerender. That's why it's safe to exclude them from dependency arrays in useEffect, useMemo, and useCallback. If you want further evidence of this, you can check out this sandbox I set up: https://codesandbox.io/s/funny-carson-sip5x
In my example, you'll see that the parent components state is changed when you click the child's button, but that the console log that would fire if the child was rerendering is not logging.
Given the above, I'd back away from the usCallback approach you are using now. I'd say it's anti-pattern. As a word of warning though, your useCallback was missing a required dependency, artistPopUp.
From there it is hard to say what is causing your component to rerender because your examples are missing key information like where the graphs, options, or records values are coming from. One thing that could lead to unexpected rerenders is if you are causing full mounts and dismounts of the parent or child component at some point.
A last note, you definitely do not need to pass that second argument to React.memo.

Call function only after multiple states have completed updating

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

componentDidUpdate not firing after the first time the component is mounted (React)

I have a function that switches windows (components) on a single page by clicking a button.
The function that swaps windows:
getProfileContent = () => {
var html = [];
if (this.state.content === "picks") {
html.push(<Picks picks={this.state.picks} deletePick={this.deletePick} />);
}
if (this.state.content === "api") {
html.push(<Admin admin="admin" />);
}
if (this.state.content === 'settings') {
html.push(<Settings />);
}
return html;
};
The content defaults to "picks" when the parent component initially loads and the first time the "Picks" component loads everything works fine because the componentDidUpdate update function below is triggered:
"Picks" component update function:
componentDidUpdate(prevProps, prevState) {
console.log('here')
if (Object.keys(prevProps.picks).length !== Object.keys(this.props.picks).length) {
this.sortPicks();
}
}
However, after swapping windows via getProfileContent and coming back to the "Picks" component the componentDidUpdate function is not triggered. I have also tried adding a different "key" value to the Picks component in hopes the new prop would trigger the componentDidUpdate, but no luck. I have the console log outside if the condition so I know componentDidUpdate isn't being called regardless of the condition. Any help is appreciated, thanks!
This question has been solved with help from #lanxion. Basically, the first time the component mounts the componentDidUpdate function is called, but only because of the parent component updating and passing in new props. The second time the component is mounted the parent already has the correct props, thus only componentDidMount is called and not componentDidUpdate. Placing the code in componentDidMount and componentDidUpdate (with conditionals) solved my issue,
It is possible that the props values are not changing, thus the same props values are being passed down and thus have the same length. Quite possibly it IS reaching the componentDidUpdate() hook, but the condition returns false and thus you don't go into the sortPicks() function. Hard to say without knowing the rest of the code.

What are the differences in setting state inside children versus sending as props?

I have got 2 pieces of code one is using independent state in child and the other one with state being send out as props like this in my " sent as props " version.
function App() {
const [isChanged, setIsChanged] = React.useState(false);
const [i, setI] = React.useState(0);
React.useEffect(() => {
console.log("USEEFFECT");
setTimeout(() => {
setIsChanged(true);
}, 2000);
}, []);
return (
<div className="App zx">
{isChanged && <h1>Hello CodeSandbox with outter states</h1>}
<Change set={setI} i={i} />
</div>
);
}
the second one with the exception of having states inside <Change /> as such :
function Change() {
const [i, setI] = React.useState(0);
let rnd = 9;
if (i !== rnd) {
setI(i + 1);
}
console.log(i);
The version in which state is managed inside the child, the component runs twice and I get the log two times in a row but in the one with passed down state as props I get the component running once as desired.
Why is this happening and which one is the correct practice ?
When can I safely use state in a child component without worrying about re-renders ?
Is there a way to avoid the re-render in the first version so I get
the same results despite using state in the child component ? If so please provide me with a sample.
To reproduce,
Props version : https://codesandbox.io/s/heuristic-driscoll-9m1pl
state in child version :
https://codesandbox.io/s/confident-shockley-btjg6
Usually you put the state in the component where you want to use it. For example, if just one child uses the state, put it right in the child component. But let's say you want to use the same state in some different child components, then it's better to have it in the parent component. (in this case, it would be better to use useContext() hook).
Honestly, I don't understand what you want to accomplish with your code but in general, rendering and re-rendering happens when you update your state. The reason it re-render agin and you see 0 to 9 and again 0 to 9 is that your default state is 0 and each time it get's re-rendered, the state changes to 0. (I assume)
Hope this answers some of your questions.

Cant change variable with callback function but console logging works

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

Categories