React updating a components state from another component causes infinite loop - reactjs

I have what is basically a form wizard with multiple steps. The wizard is divided into two parts: labels and contents. When a content component changes its internal state, from say incomplete to error or something, I want the wizard to update the label. The issue I am running into is grabbing the state from the contents component, trying to save that in the wizard component as a state so I can update the labels is causing an infinite loop.
While I understand the problem I don't really know how to solve it. This is a very minimal example and my real components use some advanced features like cloneElement to pass props to user components without them having to worry about setting 10 different props. So far this has worked flawlessly until now.
So I understand that each time I update my main components state, it's going to re-render the children, which will call the same set state function forever. What can I do instead?
import React from "react";
import "./styles.css";
// This component containsLabel children
interface LabelState {
error: boolean;
}
interface LabelGroupProps {
states: LabelState[],
}
const LabelGroup = (props: LabelGroupProps) => {
return (<div>I am Comp A</div>)
}
// This component contains Component children
interface ContentState {
error: boolean;
}
interface ContentGroupProps {
getStates: (state: ContentState, index: number) => void
}
const ContentGroup = (props: ContentGroupProps) => {
// Indicate that step 2 has an error
props.getStates({
error: true
}, 2)
return (<div>I am Comp B</div>)
}
// This is the main wizard that contains both above components
// When the state of the a content component changes, the labels must be updated.
const App = () => {
const [states, setStates] = React.useState<LabelState[]>([]);
const getStates = (state: ContentState, index: number) => {
// This causes an infinite loop
// The intention is to save this state and then update the labels
const temp = [];
temp[index] = state;
setStates(prev => [...prev, ...temp])
}
return (
<div className="App">
<LabelGroup states={states}/>
<ContentGroup getStates={getStates}/>
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-1ypi6

Your issue is caused by updating state every time ContentGroup renders. Any time there is a change in props or state, a component will automatically re-render. Then the component updates state again, then we render again, and so on. We can fix that.
What is your intention with calling getStates? You say:
// Indicate that step 2 has an error
An error could be in response to some event. For example, the user has clicked 'submit' on a form and you have a custom validation error. That might look like this:
const ContentGroup = (props: ContentGroupProps) => {
// Indicate that step 2 has an error
return (
<form
onSubmit={(event) => {
event.preventDefault();
const someErrorCondition = /* snip */;
props.getStates({
error: someErrorCondition
}, 2);
}}
>
{/* snip */}
</form>
)
}
This would not cause an infinite loop in the render because a change in state does not cause ContentGroup to call the onSubmit handler again.
Sometimes you want to perform initialization and cleanup as a component mounts and unmounts:
const ContentGroup = (props: ContentGroupProps) => {
// Indicate that step 2 has an error
React.useEffect(() => {
props.getStates({
error: true
}, 2);
return function cleanup() {
props.getStates({
error: false
}, 2);
};
}, []);
return (
<form
onSubmit={(event) => {
event.preventDefault();
/* snip */
}}
>
{/* snip */}
</form>
)
}
This does not cause an infinite loop because getStates will only be called twice: when the component mounts and right before the component unmounts.
Please treat this as pseudo-code because there are more details to leveraging hooks. Here's the documentation for useEffect, https://reactjs.org/docs/hooks-reference.html#useeffect The example code I posted won't pass many linters because it does not declare getStates as part of the hook's dependency array. In this one case, it's actually fine because we're only going to run the effect once. Further changes to getStates will be ignored. Here are more details about the dependency array of a hook: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

Related

Why is my boolean state value not toggling?

I know there are other articles and posts on this topic and almost all of them say to use the ! operator for a Boolean state value. I have used this method before but for the life of me I can not toggle this Boolean value.
import { useState } from 'react';
const [playerTurn, setPlayerTurn] = useState(true);
const changePlayerTurn = () => {
console.log(playerTurn); // returns true
setPlayerTurn(!playerTurn);
console.log(playerTurn); // also returns true
};
changePlayerTurn();
I have also tried setPlayerTurn(current => !current), commenting out the rest of my code to avoid interference, and restarted my computer in case that would help but I am still stuck with this issue.
Can anyone point out why this is not working?
The setPlayerTurn method queues your state change (async) so reading the state directly after will provide inconsistent results.
If you use your code correctly in a react component you will see that playerTurn has changed on the next render
You creating a async function, to solve this you can create a button in your component, which will run the function and you can use the "useEffect" hook to log every time the boolean changes... so you can see the changes taking place over time, like this:
import React, { useEffect } from "react";
import { useState } from "react";
const Player = () => {
const [playerTurn, setPlayerTurn] = useState(true);
useEffect(() => {
console.log(playerTurn);
}, [playerTurn]);
return <button onClick={() => setPlayerTurn(!playerTurn)}>change player turn</button>;
};
export default Player;
This is happening because setPlayerTurn is async function.
You can use another hook useEffect() that runs anytime some dependencies update, in this case your playerTurn state.
export default YourComponent = () => {
const [playerTurn, setPlayerTurn] = useState(true);
useEffect(() => {
console.log('playerTurn: ', playerTurn);
}, [playerTurn]);
const changePlayerTurn = () => {
setPlayerTurn(!playerTurn);
}
return (
<button onClick={changePlayerTurn}>Click to change player turn</button>
);
}
Basically whenever you use setState React keeps a record that it needs to update the state. And it will do some time in the future (usually it takes milliseconds). If you console.log() right after updating your state, your state has yet to be updated by React.
So you need to "listen" to changes on your state using useEffect().
useEffect() will run when your component is first mounted, and any time the state in the dependencies array is updated.
The value of the state only changes after the render. You can test this like:
// Get a hook function
const Example = ({title}) => {
const [playerTurn, setPlayerTurn] = React.useState(true);
React.useEffect(() => {
console.log("PlayerTurn changed to", playerTurn);
}, [playerTurn]);
console.log("Rendering...")
return (<div>
<p>Player turn: {playerTurn.toString()}</p>
<button onClick={() => setPlayerTurn(!playerTurn)}>Toggle PlayerTurn</button>
</div>);
};
// Render it
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
The callback inside the useEffect runs during the component mount and when one of the values inside the second argument, the dependecy array, changes. The depency here is playerTurn. When it changes the console will log.
As you will see, before this happens, the "Rendering..." log will appear.

Pass function to Context API

I'm dealing with a mix of function components and class components. Every time a click happens in the NavBar I want to trigger the function to validate the Form, it has 5 forms, so each time I'm going to have to set a new function inside the context API.
Context.js
import React, { createContext, useContext, useState } from "react";
const NavigationContext = createContext({});
const NavigationProvider = ({ children }) => {
const [valid, setValid] = useState(false);
const [checkForm, setCheckForm] = useState(null);
return (
<NavigationContext.Provider value={{ valid, setValid, checkForm, setCheckForm }}>
{children}
</NavigationContext.Provider>
);
};
const useNavigation = () => {
const context = useContext(NavigationContext);
if (!context) {
throw new Error("useNavigation must be used within a NavigationProvider");
}
return context;
};
export { NavigationProvider, useNavigation, NavigationContext};
Form.js
import React, { Component } from "react";
import { NavigationContext } from "../hooks/context";
class Something extends Component {
static contextType = NavigationContext;
onClickNext = () => {
// This is the funcion I want to set inside the Context API
if(true){
return true
}
return false;
};
render() {
const { setCheckForm } = this.context;
setCheckForm(() => () => console.log("Work FFS"));
return (
<>
<Button
onClick={this.onClickNext}
/>
</>
);
}
}
export default Something;
The problem when setting the function it throws this error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
And setting like setCheckForm(() => console.log("Work FFS"));, it triggers when rendered.
Render method of React.Component runs whenever state changes and setCheckForm updates the state whenever that render happens. This creates an infinite loop, this is the issue you are having there.
So, this is a lifecycle effect, you have to use that function inside componentDidMount if you want to set it when the component first loads.
While this solves your problem, I wouldn't suggest doing something like this. React's mental model is top to bottom, data flows from parent to child. So, in this case, you should know which component you are rendering from the parent component, and if you know which component to render, that means you already know that function which component is going to provide to you. So, while it is possible in your way, I don't think it is a correct and Reactish way to handle it; and it is probably prone to break.

Why React is rendering parent element, even if changed state isn't used in jsx? (Using React Hooks)

Im doing a React small training app using Hooks. Here's the example:
There is a MainPage.js and it has 3 similar child components Card.js. I have global state in MainPage and each Card has its own local state. Every Card has prop "id" from MainPage and clickButton func.
When I click button in any Card there are 2 operations:
Local variable 'clicked' becomes true.
The function from parent component is invoked and sets value to global state variable 'firstCard'.
Each file contains console.log() for testing. And when I click the button it shows actual global variable "firstCard", and 3x times false(default value of variable "clicked" in Card).
It means that component MainPage is rendered after clicking button ? And every Card is rendered too with default value of "clicked".
Why MainPage componenet is rendered, after all we dont use variable "firsCard", except console.log()?
How to make that after clicking any button, there will be changes in exactly component local state, and in the same time make global state variable "firstCard" changed too, but without render parent component(we dont use in jsx variable "firstCard")
Thanks for your help !
import Card from "../Card/Card";
const Main = () => {
const [cards, setCards] = useState([]);
const [firstCard, setFirstCard] = useState(null);
useEffect(() => {
setCards([1, 2, 3]);
}, []);
const onClickHandler = (id) => {
setFirstCard(id);
};
console.log(firstCard); // Showing corrrect result
return (
<div>
{cards.map((card, i) => {
return (
<Card
key={Date.now() + i}
id={card}
clickButton={(id) => onClickHandler(id)}
></Card>
);
})}
</div>
);
};
import React, { useState } from "react";
const Card = ({ id, clickButton }) => {
const [clicked, setClicked] = useState(false);
const onClickHandler = () => {
setClicked(true);
clickButton(id);
};
console.log(clicked); // 3x false
return (
<div>
<h1>Card number {id}</h1>
<button onClick={() => onClickHandler()}> Set ID</button>
</div>
);
};
export default Card;
You have wrong idea how react works.
When you change something in state that component will re render, regardless if you use that state variable in render or not.
Moreover, react will also re render all children of this component recursively.
Now you can prevent the children from re rendering (not the actual component where state update happened though) in some cases, for that you can look into React.memo.
That said prior to React hooks there was a method shouldComponentUpdate which you could have used to skip render depending on change in state or props.

Hooks parent unmounted before children

I' working on react since few months. I started Hooks since few days (I know quite late) the thing is, compare to react component the life cycle methodes it's look like they are different on some points.
The useEffect hook can reproduce :
-componentDidMount();
-componentDidUpdate();
-componentWillUnMount();
But I observe a difference between react's component and function it's about the way how function is unmounted. I noted the unmount methode, compare to the react's component,the react's function unmount the parent before the child/ren
import React, { ReactElement, useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router-dom";
export function Child2({
count,
childrenUnmounted,
}: {
count: number;
childrenUnmounted: Function;
}): ReactElement {
useEffect(() => {
return () => {
console.log("Unmounted");
childrenUnmounted(count);
};
}, [, count]);
return (
<div>
<h2>Unmouted</h2>
</div>
);
}
export function Child1({ count }: { count: number }): ReactElement {
const [validation, setValidation] = useState(false);
const usehistory = useHistory();
const childrenUnmounted = (count: number) => {
console.log("validation", validation, count);
setValidation(false);
};
const changeUrl = () => {
setValidation(true);
usehistory.push("http://localhost:3000/${count}");
};
return (
<div>
<h2>incremente</h2>
<Child2
count={count}
childrenUnmounted={(count: number) => childrenUnmounted(count)}
/>
<button className="button" onClick={() => changeUrl()}>
validation
</button>
<button
className="button"
onClick={() => usehistory.push(`http://localhost:3000/${count}`)}
>
nope
</button>
</div>
);
}
export default function Parent(): ReactElement {
const [count, setcount] = useState(-1);
const location = useLocation();
useEffect(() => {
setcount(count + 1);
}, [, location]);
return (
<div>
<h2>hello</h2>
<h3>{count}</h3>
<Child1 count={count} />
</div>
);
}
With the code above something annoying happen, when you clicked on the validation button. Value in the Child1is at true, at the moment of the click, and it's change the URL to trigger a rerender of the Parent to change the data (here count).
The thing I don't understand is why at the unmount of the Child2, at the childrenUnmounted(count) called (to trigger the same function but in the Child1) in the Child1 the validation is equal to false even the validation was clicked ? and when you click on nope just after validation you got true... it's look like the Child1 do not matter of the current state of the validation (he use the previous state)
Someone could help me to understand what's going on ?
Thx of the help.
SOLUTION:
I used useRef instead of useState from the validation to don't depend of the re-render as Giovanni Esposito said :
because hooks are async and you could not get the last value setted for state
So useRef was my solution
Ciao, I think you problem is related on when you logs validation value. I explain better.
Your parent relationship are: Parent -> Child1 -> Child2. Ok.
Now you click validation button on Child2. validation button calls changeUrl that calls usehistory.push("http://localhost:3000/${count}"); and starts to change validation value (why starts? because setValidation is async).
If the unmounting of Child2 comes now, could be that validation value is no yet setted by async setValidation (and log returns the old value for validation).
Well, at some point this setValidation finished and sets validation to true. Now you click nope button and you get true for validation (the last value setted).
So, to make the story short, I think that what you are seeing in logs it's just because hooks are async and you could not get the last value setted for state (if you use log in this way). The only way you have to log always the last value setted is useEffect hook with value you want to log in deps list.

how should I build onClick action in my react component + Redux

I've been through many tutorials and questions on Stack but I can't find a solution. I'm just learning React/redux, trying to build OnClick action. I've got the following error "Maximum call stack size exceeded error". I got this because I'm rendering a function that's changing my state infinitely. I'm trying to deal with my <button onClick={DisplayTable(click)}>cool</button> differently but nothing seems to work.
I also know that my action and I guess my reducers works properly since when I'm dispatching my action trough the console : $r.store.dispatch({type: 'SET_TABLE_DATA'});, my state is updated properly.
Any advices ?
here is my action :
export const setTableFilter = (click) => {
return {
type: 'SET_TABLE_DATA',
click : click,
};
};
here is my reducer :
const tableFilter = (state = 0, action) => {
if(action.type === 'SET_TABLE_DATA') {
return state + 1;
}
return state;
}
and here is my component :
const DisplayTable = (click) => {
return (
<div>
<button onClick={DisplayTable(click)}>cool</button>
</div> )
}
function mapStateToProps(state) {
return {
click: state.tableFilter.click
};
};
const mapDispachToProps = (dispatch) => {
return {
DisplayTable: (click) => {dispatch (setTableFilter(click));
},
};
};
const AppTable = connect(mapStateToProps, mapDispachToProps)(DisplayTable);
export default AppTable;
I also know that I should build my reducer in a way that my state should be updated without any mutation, however I'll keep this for later ! :)
thanks.
The answer given doesn't really explain why your code was not working, so I thought I'd expand on that.
Your problem is that you are exceeding the function call stack, more commonly known as infinite recursion. The reason this is happening is because you aren't passing a function to the onClick attribute of your button, but rather invoking a function and passing its return value instead. So the following scenario is happening:
React component is mounted to the DOM
render() is called
The DisplayTable function is invoked, which dispatches an update to the store
The store updates, and passes new props to the React component
render() is called again
DisplayTable is invoked again
...and so on.
What you'll want to do instead is pass the function to the button's onClick attribute. So your component should look like this:
const Component = props => {
return (
<div>
<button onClick={props.DisplayTable}>cool</button>
</div>
);
};
In that above code snippet, I removed your click prop because it doesn't look like you're using it at all (given the code you posted in the OP).
A few tips, not a complete solution since that would not help you learn:
Your action and reducer are looking fine. You are passing the click property which is not used in the reducer. Maybe you will use it in the future but for now it is useless.
A React component function takes props as an argument:
const Comp = props => {
const click = props.click;
// ...
};
mapDispatchToProps is usually not needed. Use plain objects instead:
connect(state => state.tableFilter, { setTableFilter })(DisplayTable);
You can then access the function from props:
<button onClick={() => props.setTableFilter(click)}>cool</button>
Keep in mind: onClick takes a function!
Also the state you defined in the reducer has no property called click, instead it is a number (see correct mapStateToProps function above)

Resources