Prevent infinite loop when updating state within render() - reactjs

I have the following use case where I have an input and whenever that changes, I need to re-calculate some things and re-render them.
My problem is that I need to update the state in a function called within render(). However, that would case an infinite loop since every such update would cause render() to re-run.
class Test extends Component {
state= {
inputVal='a'
count=0
}
inputChanged = event => this.setState({inputVal: event.target.value});
calculateThingsToBeRendered = () => {
// the value for "count" is calculated (using a hardcoded value to simulate)
let localCount = 5
// need to update the value of the state's "count" here (render() will re-run)
return (
<div> Irrelevant thing to render </div>
);
}
render() {
let thingsToRender = this.calculateThingsToBeRendered();
return {
<div>
<input type="text" onChange={this.inputChanged} value={this.state.inputVal} />
{this.state.count}
<div>
{thingsToRender}
</div>
</div>
}
}
}
Using shouldComponentUpdate wouldn't satisfy my needs since the updated value for the count wouldn't update in the UI.
Moving the calculations done within the calculateThingsToBeRendered (so that the count part of the state gets updated there) to inputChanged wouldn't work because I need the JSX returned by the same function in render.

I don't get in the first place why you "need" to run a function that updates the state in the render function.
I guess you're trying to calculate something from the input.
So there is an state update from input and you want to calculate that afterwards an put that into the state (which is now in a function used in render)
one of many options is to calculate your new state when the event happens.
class Test extends React.Component {
state = {
inputVal: "a",
count: 0
};
inputChanged = event => this.setState({
inputVal: event.target.value,
count: this.calculate(event.target.value)
});
thingsToBeRendered = () => {
return <div> {this.state.count} </div>;
};
calculate = (value) => {
// update your stuff here
return value.length()
}
render() {
let thingsToRender = this.thingsToBeRendered();
return (
<div>
<input
type="text"
onChange={this.inputChanged}
value={this.state.inputVal}
/>
{this.state.count}
<div>{thingsToRender}</div>
</div>
);
}
}

Related

Pass state as props from parent to child and then update parent from child onSubmit

I have a parent component that renders a child component and passes it's initial state to the child component. I need a few clarifications- My gut feeling is to handle the event change in the Child component, is this correct? Upon submission, how do I pass the updated props back to parent? My gut is also telling that once the props is passed back to the parent, I can use componentDidUpdate() to set the state to be used elsewhere. If so, how?
class Parent extends React.Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "destination"
};
}
componentDidUpdate(){
// how to update state?
}
render() {
const { arrival, departure } = this.state;
return <Child arrival={arrival} departure={departure} />;
}
}
class Child extends React.Component{
constructor(){
this.handleSubmission = this.handleSubmission.bind(this);
}
handleSubmission(e){
const target = e.target;
const name = target.name;
const value = target.value;
// not sure how to handle props from here
}
render(){
let { arrival, departure } = this.props;
return(
<form onSubmit = {this.handleSubmission} >
<div class="form-group">
<label for="departure">Departure</label>
<input type="" class="form-control" name="departure" aria-describedby="emailHelp" placeholder="Enter Departing Station"/>
</div>
<div class="form-group">
<label for="arrival">Arrival</label>
<input type="password" class="form-control" name="arrival" id="inputArrival" placeholder="Enter Arriving Station"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
)
}
}
export default Child
#hello_there
Please disregard the previous answer, as it requires a lot more than just changing a few props.
I've forked the Sandbox and rewrote it here (so you can follow along).
I've outlined the steps to make the change to propagate to the parent below.
Capture the states of <input />
First step is to capture the state of form fields.
There are two ways to handle form fields
Controlled Components
Uncontrolled Components - discouraged
I am going to use the former (controlled) to capture the form field states by adding a state to Form component.
And you need to set the value={...} of each state and update each state from onChange event (using handleInputChange added below) for each form field.
I've added 👇 where changes were made
import React, { Component } from "react";
class Form extends Component {
// .. 👇
state = {
departure: "",
arrival: ""
};
//... rest removed for brevity
// .. 👇 is used to update each form field state.
handleInputChange = e => {
e.preventDefault();
const { name, value } = e.target;
this.setState({ [name]: value });
};
render() {
const { departure, arrival } = this.state;
return (
<form onSubmit={this.handleSubmission}>
<div className="form-group">
<label> Departure</label>
<input
className="form-control"
name="departure"
placeholder="Enter Departing Station"
// ... 👇 ...👇
value={departure} onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label> Arrival</label>
<input
className="form-control"
name="arrival"
id="inputArrival"
placeholder="Enter Arriving Station"
// ... 👇 ...👇
value={arrival} onChange={this.handleInputChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
}
Update App's state change event handler
Now we have states handy, we need to update the App's updateState to accept a whole new state, so we don't make multiple method calls (this.props.updateParentState) and it'd let us pass a new reference so that React would know that the state has been changed in the App component.
class App extends Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "departure"
};
}
// From this 👇
// updateState = (name, value) => {
// this.setState({
// [name]: value
// });
// };
// to this 👇
updateState = newState => this.setState(newState);
componentDidUpdate(prevProps, prevState) {
const { arrival, departure } = this.state;
console.log(`Arrival: ${arrival}, departure: ${departure}`);
}
render() {
const { arrival, departure } = this.state;
return (
<Fragment>
<Form
arrival={arrival}
departure={departure}
// 👇 stays the same
updateParentState={this.updateState}
/>
</Fragment>
);
}
}
Update Child's submission event handler
Now the App.updateState accepts a state object, which can be used to update App.state, let's change Child.handSubmission.
handleSubmission = e => {
e.preventDefault();
// this.props.updateParentState(name, value);
this.props.updateParentState(this.state);
};
You can see that this.props.updateParentState(name, value) has been replaced with this.props.updateParentState(this.state), which would let us update App.state at once.
Now you should be able to see the change in the App.componentDidUpdate.
OLD ANSWER - Disregard this
Changing the state in the child doesn't cause the re-render in the parent (so componentDidUpdate is probably not triggered by change in child component). So what you can do is to pass the event handler down to the child, which the child can notify the Parent component that something has changed.
I've explained the flow of how you can update the parent's states.
First you need to create a handler, with which you can update the Parent's state with.
updateState = (name, value) => this.setState({ [name]: value });
Here, I am using [name], which is a dynamic property. So it should match up with the Parent's state name. In this case either arrival or departure.
Then you need to pass that event handler to the Child (you can name the prop name to whatever (In the code below, I used updateParentState but it can be updateWhatever so long as you pass it the updateState correctly).
<Child arrival={arrival} departure={departure} updateParentState={updateState} />
Here is the complete code that will update the parent state.
Changes are indicated with "👇" emoji below.
class Parent extends React.Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "destination"
};
}
// This is what you pass to the child as a `prop` to call.
updateState = (name, value) => this.setState({ [name]: value });
render() {
const { arrival, departure } = this.state;
// pass the event handler to the child component ... 👇 ...
return <Child arrival={arrival} departure={departure} updateParentState={updateState} />;
}
}
class Child extends React.Component{
constructor(){
this.handleSubmission = this.handleSubmission.bind(this);
}
handleSubmission(e){
const target = e.target;
const name = target.name;
const value = target.value;
// 👇 Update the "Parent"'s state.
// `name` & `value` will be the supplied to `Parent.updateState`
this.props.updateParentState(name, value)
}
render(){
// ... removed for brevity
}
}
If the statement management gets too hard to manage with prop-drilling, then you can reach out for Context API (after familiarizing with it, you can check out How to use React Context effectively, which uses Hooks).
Or you can use Redux.
Just pass to your children a reference on how to update the state:
class Parent extends React.Component{
state = {departure : ''}
setDeparture = departure => this.setState({ departure })
render(){ <Child setDeparture={this.setDeparture} /> }
}
const Child = ({setDeparture}) => <button onClick={() => setDeparture('foo')}>Click</button>

Does it anti-pattern to change state parameter in render?

I need to make simple toggle animation, if state in component was changed. But I don't want to show this animation each time when react calls component render. I found a way how to do it, but I don't like that I need to change state variable directly without setState and make it in render. Also I don't like idea to generate new key each time on animation. I think it it anti-pattern. How can I achieve same result but with different way?
onClick() {
if(this.props.items.length>=5) {
this.setState({
toggleWarning: true
});
}else {
this.props.onStepAdd();
}
}
render() {
let toggle = "";
let toggleKey = null;
if(this.state.toggleWarning) {
toggle = " "+style.toggleMsg;
toggleKey = {key:Math.random()};
this.state.toggleWarning = false;
}
return (
<div>
<h1>Add New</h1>
<div>
<Button
onClick={this.onClick}
label="Add Step"
/>
</div>
<div
className={style.noteMsg + toggle}
{...toggleKey} >
<h3>Note: You can create only up to 5 steps for form wizard</h3>
</div>
</div>
);
}
not sure I understand the objective, but if you want to add a class after a click and then remove the class after some time:
previousTimeout = undefined
onClick = () => {
if(this.props.items.length>=5) {
this.setState({toggleWarning: true});
clearTimeout(this.previousTimeout);
this.previousTimeout = setTimeout(() => {
this.setState({toggleWarning: false})
}, 500);
}
render() {
...className={style.noteMsg + this.state.toggleWarning ? ' '+style.toggleMsg : ''}
}

React Parent component checkbox state updates with one step delay

I have a Parent component:
import React, { Component } from "react";
import { Button } from "./Button";
export class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {
numbers: [],
disabled: false
};
this.setNum = this.setNum.bind(this);
}
setNum(num) {
if (!this.state.numbers.includes(num)) {
this.setState(prevState => ({
numbers: [...prevState.numbers, num]
}));
} else if (this.state.numbers.includes(num)) {
let nums = [...this.state.numbers];
let index = nums.indexOf(num);
nums.splice(index, 1);
this.setState({ numbers: nums });
console.log(this.state.numbers);
}
if (this.state.numbers.length >= 4) {
this.setState({ disabled: true });
} else if (this.state.numbers.length < 4) {
this.setState({ disabled: false });
}
}
render() {
return (
<div className="board-container">
<div className="board">
<div className="row">
<Button
id="1"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
<Button
id="2"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
<Button
id="3"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
<Button
id="4"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
</div>
</div>
</div>
);
}
}
... and a Child component:
import React, { Component } from "react";
export class Button extends Component {
constructor(props) {
super(props);
this.state = {
isChecked: false
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({
isChecked: !this.state.isChecked
});
var num = e.target.value;
this.props.onChange(num);
}
render() {
const { isChecked } = this.state;
if (isChecked === true) {
var bgColor = "#f2355b";
} else {
bgColor = "#f7f7f7";
}
let disabled = this.props.disabled;
if (this.props.numbers.includes(this.props.id)) {
disabled = false;
}
return (
<div className="number-container" id="checkboxes">
<label
className={!isChecked && disabled === false ? "num" : "checked-num"}
style={{ backgroundColor: bgColor }}
>
{" "}
{this.props.id}
<input
type="checkbox"
name={this.props.id}
value={this.props.id}
id={this.props.id}
onChange={this.handleChange}
checked={isChecked}
disabled={disabled}
/>
</label>
</div>
);
}
}
Whenever any Button component is clicked, the Parent component gets the child Button's id value and puts it into its numbers state array. Whenever a Button is unchecked, the Parent updates is numbers state by removing the id of the child Button.
If my code is right, the expected behavior is whenever a Button checkbox is clicked, the Parent numbers state will be updated immediately (adding or removing a number). However, it always updates with one step lag behind.
I know, that the issue is dealing with the React states not being updated instantly, and I've checked similar issues on Stackoverflow. The problem is that I can't figure it out how to make this two components interact with each other in a proper way. What would be the solution for this issue?
Here are three screenshots from codesandbox
If you want to play with it please find the link https://codesandbox.io/s/w2q8ypnxjw
What I did was, I basically copied and pasted your code and updated setNum function to reflect the changes Think-Twice suggested
setNum(num) {
if (!this.state.numbers.includes(num)) {
this.setState(
prevState => ({
numbers: [...prevState.numbers, num]
}),
() => {
console.log("state logged inside if", this.state.numbers);
}
);
} else if (this.state.numbers.includes(num)) {
let nums = [...this.state.numbers];
let index = nums.indexOf(num);
nums.splice(index, 1);
this.setState({ numbers: nums }, () => {
console.log("state logged inside else if", this.state.numbers);
});
}
if (this.state.numbers.length >= 4) {
this.setState({ disabled: true });
} else if (this.state.numbers.length < 4) {
this.setState({ disabled: false });
}
}
So before going further let's quickly address a couple of things regarding to React and setState
As B12Toaster mentioned and provided a link which contains a
quote from official documentation
setState() does not always immediately update the component. It may
batch or defer the update until later.
Think-Twice's also points out that by stating
Basically setState is asynchronous in React. When you modify a value
using setState you will be able to see the updated value only in
render..
So if you want to see the immediate state change in a place which
you trigger setState, you can make use of a call back function as
such setState(updater[, callback])
There are two approaches when it comes to and updater with setState,
you could either pass an object, or you could pass a function So in
Think-Twice's example, an object is passed as an updater
this.setState({ numbers: nums } //updater, () => {
console.log(this.state.numbers); //this will print the updated value here
});
When a function is used as an updater (in your setNum function you
already do that), the callback function can be utilized like below
if (!this.state.numbers.includes(num)) {
this.setState(
prevState => ({
numbers: [...prevState.numbers, num]
}),
() => {
console.log("state logged inside if", this.state.numbers);
}
);
}
Your current implementation and communication structure seems fine. It is actually called Lifting State Up which is recommended also by official documentation.
Basically you store the state of array numbers in a parent component (which can be considered as the source of truth) and you pass the method that changes the state as a prop to it's child component.
In the codesandbox link I provided, the functionalities works the way I expect (at least this is what I expect from your code)
Basically setState is asynchronous in React. When you modify a value using setState you will be able to see the updated value only in render. But to see updated state value immediately you need to do something like below
this.setState({ numbers: nums }, () => {
console.log(this.state.numbers); //this will print the updated value here
});

maximum call stack error using setState in render function

I'm starting to learn it but can't find a solution for this -> I have an input and when the value exceeds 20 characters I want a tooltip to appear with the full value as is typed. I have it all built and kind of working. Problem is that I get a maximum call stack error because the state is being changed on every key press - I'm not sure of the best/correct way to go about it... any help would be greatly appreciated
See my code below and here is the pen
console.clear();
class Input extends React.Component {
render(){
return(
<div>
<input
className="main-input"
onChange={this.props.onChange}
placeholder={"Tell me something"}
/>
</div>
)
};
}
class Tooltip extends React.Component {
render(){
return(
<div
className="tooltip"
data-status={this.props.isShowing}>
<p>{this.props.TooltipValue}</p>
</div>
)
}
}
class App extends React.Component {
constructor(){
super();
this.state = {
message: '',
isShowing: false
}
}
onChange(e) {
this.setState({
message: e.target.value
});
}
render(){
const inputVal = (this.state.message.length);
if(inputVal >= 20){
this.setState({isShowing: true})
}
// else {
// this.setState({isShowing: false})
// }
return(
<div className="container">
<Tooltip
TooltipValue={this.state.message}
isShowing={this.state.isShowing}
/>
<Input onChange={this.onChange.bind(this)}/>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('Main')
)
Why maximum call stack error when using setState in render function ?
Because when we do setState, React trigger the re-rendering of the component, if you do setstate inside render then after setState, component will render again, again it will find setState again render, it will become a infinite loop, So never do any setState inside render method.
Check this answer for more details about setState behaviour.
Instead of putting that check inside render method, put that inside onChange method, like this:
onChange(e) {
const inputVal = e.target.value;
this.setState({
message: inputVal,
isShowing : inputVal.length > 20 ? true : false
});
}
And remove these lines:
if(inputVal >= 20){
this.setState({isShowing: true})
}
// else {
// this.setState({isShowing: false})
// }
Or since the showing of Tooltip depends on the value of input element, you can avoid extra variable and directly check the length of input element with Tooltip property isShowing, like this:
<Tooltip
TooltipValue={this.state.message}
isShowing={this.state.message.length >= 20}
/>
No need to use a separate state for the tooltip since it already depends on the state value you can do it like this
<Tooltip
TooltipValue={this.state.message}
isShowing={this.state.message.length > 20} >
Also since you are using setState in the render() you are getting an error because setState triggers a re-render and thus beginning an infine loop as soon as you if-condition becomes true.
CODEPEN

How to allow updates to input without updating props in React?

I would like to allow a user to enter changes in an input field without propagating them to the parent. I did this by returning out of the onChange function whenever I don't want to propagate. However this seems to undo the character I typed.
Here is a use case. I have a number field. I want to trigger onChange on parent when there is a number entered, but ignore "." and ","s (formatters.staticToFloat removes them).
export default class NumberField extends React.Component {
render () {
var props = this.props;
var format = props.formatter || formatters.number;
return (
<div>
<label>{props.inputLabel}</label>
<input
type="text"
name={props.name}
onChange={this.onChange.bind(this)}
value={!_.isUndefined(props.value) ? format(props.value) : null}
/>
</div>
);
}
onChange (e) {
var numValue = formatters.stringToFloat(e.target.value);
//they added a . or , we don't propagate change
if (this.props.value === numValue) {
return;
}
if (this.props.onChange) {
this.props.onChange({
value: numValue,
valid: validation.isValid(numValue, this.props.validation)
});
}
}
};
So far the best way I've come up with is maintaining a separate formattedValue state that only gets set when I want to override the default formatting. It works, but seems like a super dirty solution.
export default class NumberField extends React.Component {
constructor () {
super();
this.state = {
formattedValue: null
};
}
render () {
var props = this.props;
var state = this.state;
var format = props.formatter || formatters.number;
var inputValue = state.formattedValue || (
!_.isUndefined(props.value) ?
format(props.value) :
null
);
return (
<div>
<label>{props.inputLabel}</label>
<input
type="text"
name={props.name}
onChange={this.onChange.bind(this)}
value={inputValue}
/>
</div>
);
}
onChange (e) {
var numValue = formatters.stringToFloat(e.target.value);
//they added a . or , we don't propagate change
if (this.props.value === numValue) {
this.setState({
formattedValue: e.target.value
});
} else {
this.setState({
formattedValue: null
});
}
if (this.props.onChange) {
this.props.onChange({
value: numValue,
valid: validation.isValid(numValue, this.props.validation)
});
}
}
};
Sounds like a case for using state to store the formatted value inside the component, and use a special variant of setState with a callback.
all user input (including . and ,) is put in state and rendered in input field back to user.
only when the format is correct, the parent onChange() is called.
The parent may actually do a re-render triggered by the onChange() call. Therefore, we need to make sure that the last entered character is updated in state, and only after that the parent's onChange() will be called.
Your component would look like as follows:
export default class NumberField extends React.Component {
constructor (props) {
super(props);
this.state = {
value: !_.isUndefined(props.value) ?
props.formatter ? props.formatter(props.value).toString() : formatters.number(props.value).toString()
: null;
};
}
render () {
var props = this.props;
var state = this.state;
return (
<div>
<label>{props.inputLabel}</label>
<input
type="text"
name={props.name}
onChange={this.onChange.bind(this)}
value={state.value}
/>
</div>
);
}
onChange (e) {
var numValue = formatters.stringToFloat(e.target.value);
// if numValue is different from current state
// then it must be an OK update,
// so we update state AND call parent if function exists
if (numValue.toString() != this.state.value) {
this.setState({
value: numValue
},
this.callParent // here is the magic: we pass a callback, to be called after state update and after re-render
);
} else {
// otherwise we only update state (to display invalid character)
this.setState({
value: numValue
});
}
}
callParent() {
// state is updated and component has re-rendered when this is called
// so we can use state.value to inform parent
if (this.props.onChange) {
this.props.onChange({
value: this.state.value,
valid: validation.isValid(this.state.value, this.props.validation)
});
}
}
};
This may be a bit of overkill: you keep a state (formatted value) that you also communicate to the parent. If your parent ALWAYS passes down the newly forwarded input, then you could make your component a lot simpler:
No state, but simply re-render based on props.
You only really need state if the value presented to the user in the input field can deviate from what you communicate to parent.

Resources