Incremental React Game with setInterval: Cannot update during existing state transition - reactjs

Im trying to get this code working as a "incremental" game, where the options will be displayed after some credits, but Im getting the following warning (only once):
Warning: Can't call setState (or forceUpdate) on an unmounted component.
index.js:2178 Warning: Cannot update during an existing state
transition (such as within render or another component's
constructor). Render methods should be a pure function of props and
state; constructor side-effects are an anti-pattern, but can be moved
to componentWillMount.
Here is the code:
import React, { Component } from 'react';
import { Grid, Button } from "semantic-ui-react";
class EventDashboard extends Component {
constructor(props) {
super(props)
this.state={
credits: 0,
newOption: false,
newOptionDirty: false,
}
this.addCredits = this.addCredits.bind(this)
this.renderNewOption = this.renderNewOption.bind(this)
this.intervalID = null;
}
addCredits() {
this.setState((previousState) => ({
credits: previousState.credits + 1
}))
}
componentDidMount() {
this.intervalID = setInterval(() => {
this.addCredits()
}, 1000)
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
renderNewOption() {
if(this.state.credits === 5 && !this.state.newOptionDirty) {
// the warning happens here
this.setState(() =>({
newOptionDirty: true
}))
}
if(this.state.newOptionDirty) {
return(
<div>
<Button> Add new feature </Button>
</div>
)
} else {
return (
<div>no options until 5 credits</div>
)
}
}
render() {
return (
<Grid>
<Grid.Column width={10}>
<Button
onClick={this.addCredits}
>AddCredits</Button>
{this.renderNewOption()}
</Grid.Column>
<Grid.Column width={6}>
<h1>Credits</h1>
<h2>{this.state.credits}</h2>
</Grid.Column>
</Grid>
)
}
}
export default EventDashboard
In spite of this warning everything is working fine.
What good practice Im missing?

The problem is indeed with your renderNewOption() method and how you use it.
You are invoking renderNewOption() from render(), which is bad because you are doing a setState() in it. Remember that whenever you do setState() you update the state and trigger a new render. As such, having setState() inside the render() would create an infinite loop since the render function would keep calling itself.
In this particular case, you won't actually get an infinite loop because you have an if-case that prevents that, however your component will run it the first time (when newOptionDirty is still false). When that happens, your component hasn't mounted yet because the render() never finished before the this particular setState() was invoked.
TL;DR: Never call setState() during a render.
Try this instead:
componentDidMount() {
setInterval(() => {
this.addCredits()
}, 1000);
}
addCredits() {
this.setState((previousState) => ({
credits: previousState.credits + 1
});
}
renderNewOption() {
if(this.state.credits >= 5) {
return(
<div>
<Button> Add new feature </Button>
</div>
)
} else {
return (
<div>no options until 5 credits</div>
)
}
}
We don't need (and shouldn't) to create some new state variable inside the render. All we are interested in is if credits are equal to 5 or not.
Depending on its value, we return the appropriate React Element (button or no button).

Whenever you are doing any asynchronous stuff in your component like callbacks/Promises/setTimeout you might run into a situation when a component might have already been unmounted and you will try to update umounted component ( like in your case ) which might lead to memory leaks. This is one of the use cases for external state management libraries and middlewares like redux and redux-saga/redux-observable.
If you are going to do it in your components however you need to take care to do necessary cleanups when component unmounts. This is what for componentWillUnmount is used for:
constructor(props) {
super(props)
...
this.intervalID = null;
}
componentDidMount() {
this.intervalID = setInterval(() => {
this.addCredits()
}, 1000)
}
componentWillUnmount() {
clearInterval(this.intervalID);
}

Related

ReadOnly state when I need to change it

I have the following code and I really need to be able to change the state however I am having issues when I try and do the following.
export default class Mediaplayer extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
isLoaded: false,
items: [],
station: null,
playButton: false,
muteButton: false,
};
}
render() {
const { station, playButton, muteButton } = this.state;
const handleMClick = (e) => {
// Event("Play Button", "Listner Hit Play", "PLAY_BUTTON");
console.log("clicking the play and pause button");
this.setState({ playButton: !playButton });
playButton
? document.getElementById("player").play()
: document.getElementById("player").pause();
};
return (
<i onClick={handleMClick}>
{playButton ? <PlayCircle size={60} /> : <PauseCircle size={60} />}
</i>
);
}
}
I am getting this state is ReadOnly.
setState() only takes effect after the whole eventHandler is
finished, this is called state batching.
Your this.setState({playButton:!playButton}) only run after handleMClick() is finished.
In other words, playButton === true will not available within your handleMClick() function.
On solution could be to put this:
playButton ? document.getElementById("player").play() : document.getElementById("player").pause()
Inside a componentDidUpdate() so it will take effect in the next render after your state is updated.
Direct dom manipulation is not a recommended way of doing things in react because you can always change dom element state according to your react component state or props.
I see your component is called media player but it doesn't have the #player inside it? Perhaps you could reconsider how you arranging the dom element.
Also try to use a functional component instead of class component. I will give an answer with a functional component.
MediaPlayer Component
import { useState } from 'react';
const MediaPlayer = props => {
const [play, setPlay] = useState(false);
const togglePlay = () => {
setPlay( !play );
}
return (
<i onClick={togglePlay}>
{!play ?
<PlayCircle size={60}/>
:
<PauseCircle size={60}/>}</i>
}
);
}

Render issue caused by setState executed every 0.1 sec in setInterval

I'm trying to click on a div that has the onClick function associated with it, but the function doesnt getting called due to the fact that I have a setState inside a setInterval called every 0.1 sec. This updates the DOM and doesnt let me call the function onClick.
I tried to use PureComponent and React.memo to avoid re-renders nested Components, but it didn't work; I could not use them properly though.
Inside the father constructor I have, basically, this:
setInterval(()=> {
this.setState({state1: 0})
}, 100)
}
EDIT
I'm proud of showing you the minimum (almost) code to test the issue (note the functional component: if you remove it and replace the < F /> with its content, it will work properly. Also, if you debug with google chrome, you will see what's going on the DOM):
class App extends Component {
constructor() {
super()
this.state = {state1: 0}
setInterval(() => {this.setState({state1: this.state.state1 + 1})}, 100)
}
render() {
const F = () => (
<button onClick={()=> alert("this function will be called... sometimes")}>
test: {this.state.state1}
</button>
)
return <div> <F/> </div>
}
}
EDIT 2
If I write the functional component as a pure function, it will work. here's the example:
class App extends Component {
constructor() {
super()
this.state = { state1: 0}
setInterval(() => {this.setState({state1: this.state.state1 + 1})}, 100)
}
render() {
const F = ({state1}) => (
<button onClick={()=> {alert("called sometimes")}}> test (will be called sometimes): {state1} </button>
)
function f(state1) {
return <button onClick={()=> {alert("called always")}}> test(will be called always): {state1} </button>
}
return <div> <F state1={this.state.state1}/> {f(this.state.state1)}</div>
}
}
setState will, by default rerender components that are impacted by said state.
This was answered here.
I would suggest moving away from setting state that often. That's quite expensive and I'm betting there is a far more efficient way to accomplish whatever it is that you're trying to do without the interval.
If you are using React 16.8^ then you could use Hooks to make multiple state changes. Note that instead of using setInterval i used setTimeout for each cycle
import React, {useState, useEffect} from 'react';
const App = (props) => {
const [numberShown, setNumberShown] = useState(0);
useEffect(() => {
setTimeout(() => setNumberShown(numberShown + 1), 100);
}, [numberShown]);
return (<div>
{numberShown}
</div>)
}
Hope it helps
EDIT : I found a way to do it the Component Way:
class App extends Component {
constructor() {
super()
this.state = {state1: 0}
this.startTimer = this.startTimer.bind(this)
this.startTimer()
}
startTimer = () => {
setInterval(() => {this.setState({state1: this.state.state1 + 1})}, 100)
}
render() {
return <button onClick={()=> alert("this function will be called... sometimes")}>
test: {this.state.state1}
</button>
}
}
By experimenting i noticed that if you render the button like this:
render() {
const F = () => (
<button onClick={()=> alert("this function will be called... sometimes")}>
test: {this.state.state1}
</button>
)
return <div> <F/> </div>
}
Instead of directly returning the button tag, the nested onClick function triggering the alert won't always go off but if you do it like in my example it will always trigger.

Called componentDidMount twice

I have a small react app. In App.js I have layout Sidenav and Content area. The side nav is shown on some page and hid from others. When I go to some components with sidenav, sidenav flag is set by redux and render the component again, in the componentDidMount I have api call, and it is executed twice.
class App extends Component {
renderSidebar = () => {
const {showNav} = this.props;
return showNav ? (
<TwoColumns>
<Sidenav/>
</TwoColumns>) : null;
};
render() {
const {showNav} = this.props;
const Column = showNav ? TenColumns : FullColumn;
return (
<Row spacing={0}>
{this.renderSidebar()}
<Column>
<Route exact path="/measurements/:id/:token/:locale/measure"
component={MeasurementPage}/>
</Column>
</Row>
)
}
}
const mapStateToProps = state => ({
showNav: state.sidenav.showNav
});
export default connect(mapStateToProps)(App);
I tried to use shouldComponentUpdate to prevent the second API call
class MeasurementPage extends Component {
constructor(props) {
// This update the redux "showNav" flag and re-render the component
props.toggleSidenav(false);
}
shouldComponentUpdate(nextProps) {
return !nextProps.showNav === this.props.showNav;
}
componentDidMount() {
// This is executed twice and made 2 api calls
this.props.getMeasurement(params);
}
render() {
return <h1>Some content here</h1>;
}
}
const mapStateToProps = state => ({
showNav: state.sidenav.showNav
});
export default connect(mapStateToProps)(MeasurementPage);
Did someone struggle from this state update and how manage to solve it?
This props.toggleSidenav(false) might cause side effect to your component lifecycle. We use to do this kind of stuff inside componentWillMount and it has been depreciated/removed for a reason :). I will suggest you move it inside componentDidMount
class MeasurementPage extends Component {
constructor(props) {
// This update the redux "showNav" flag and re-render the component
// props.toggleSidenav(false); // remove this
}
shouldComponentUpdate(nextProps) {
return nextProps.showNav !== this.props.showNav;
}
componentDidMount() {
if(this.props.showNav){ //the if check might not necessary
this.props.toggleSidenav(false);
this.props.getMeasurement(params);
}
}
render() {
return <h1>Some content here</h1>;
}
}
The comparison should be
shouldComponentUpdate(nextProps) {
return !(nextProps.showNav === this.props.showNav)
}
The problem is that !nextProps.showNav negate showNav value instead of negating the role expression value, and that is why you need an isolation operator.
It's No call twice anymore.
componentDidMount() {
if (this.first) return; this.first = true;
this.props.getMeasurement(params);
}

How to change a component's state correctly from another component as a login method executes?

I have two components - a sign in form component that holds the form and handles login logic, and a progress bar similar to the one on top here in SO. I want to be able to show my progress bar fill up as the login logic executes if that makes sense, so as something is happening show the user an indication of loading. I've got the styling sorted I just need to understand how to correctly trigger the functions.
I'm new to React so my first thought was to define handleFillerStateMax() and handleFillerStateMin() within my ProgressBarComponent to perform the state changes. As the state changes it basically changes the width of the progress bar, it all works fine. But how do I call the functions from ProgressBarComponent as my Login component onSubmit logic executes? I've commented my ideas but they obviously don't work..
ProgressBarComponent:
class ProgressBarComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
percentage: 0
}
}
// the functions to change state
handleFillerStateMax = () => {
this.setState ({percentage: 100})
}
handleFillerStateMin = () => {
this.setState ({percentage: 0})
}
render () {
return (
<div>
<ProgressBar percentage={this.state.percentage}/>
</div>
)
}
}
Login component:
class SignInFormBase extends Component {
constructor(props) {
super(props);
this.state = {...INITIAL_STATE};
}
onSubmit = event => {
const {email, password} = this.state;
// ProgressBarComponent.handleFillerMax()????
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
//ProgressBarComponent.handleFillerMin()????
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}
Rephrase what you're doing. Not "setting the progress bar's progress" but "modifying the applications state such that the progress bar will re-render with new data".
Keep the current progress in the state of the parent of SignInFormBase and ProgressBarComponent, and pass it to ProgressBarComponent as a prop so it just renders what it is told. Unless there is some internal logic omitted from ProgressBar that handles its own progress update; is there?
Pass in a callback to SignInFormBase that it can call when it has new information to report: that is, replace ProgressBarComponent.handleFillerMax() with this.props.reportProgress(100) or some such thing. The callback should setState({progress: value}).
Now, when the SignInFormBase calls the reportProgress callback, it sets the state in the parent components. This state is passed in to ProgressBarComponent as a prop, so the fact that it changed will cause he progress bar to re-render.
Requested example for #2, something like the following untested code:
class App extends Component {
handleProgressUpdate(progress) {
this.setState({progress: progress});
}
render() {
return (
<MyRootElement>
<ProgressBar progress={this.state.progress} />
<LoginForm onProgressUpudate={(progress) => this.handleProgressUpdate(progress)} />
</MyRootElemen>
)
}
}
The simply call this.props.onProgressUpdate(value) from LoginForm whenever it has new information that should change the value.
In basic terms, this is the sort of structure to go for (using useState for brevity but it could of course be a class-based stateful component if you prefer):
const App = ()=> {
const [isLoggingIn, setIsLoggingIn] = useState(false)
const handleOnLoginStart = () => {
setIsLoggingIn(true)
}
const handleOnLoginSuccess = () => {
setIsLoggingIn(false)
}
<div>
<ProgressBar percentage={isLoggingIn?0:100}/>
<LoginForm onLoginStart={handleOnLogin} onLoginSuccess={handleOnLoginSuccess}/>
</div>
}
In your LoginForm you would have:
onSubmit = event => {
const {email, password} = this.state;
this.props.onLoginStart() // <-- call the callback
this.props.firebase
.doSignInWithEmailAndPass(email,password)
.then(()=> {
this.setState({...INITIAL_STATE});
this.props.history.push('/');
this.props.onLoginSuccess() // <-- call the callback
})
.catch(error => {
this.setState({error});
})
event.preventDefault();
}

Rerender not working after states updated

I have a parent component Rides which sends data to child component via props.
Here is the Child component:
class LazyLoader extends React.Component {
constructor(props){
super(props);
this.state = {
loaderState: this.props.loaderState
}
}
componentWillReceiveProps(nextProps){
console.log(`inside componentWillReceiveProps ${nextProps.loaderState}`);
if(this.props != nextProps) {
this.setState({
loaderState: nextProps.loaderState
}, () => {console.log(`Finished setState for ${nextProps.loaderState}`)});
}
}
render(){
console.log(`loaderState: ${this.state.loaderState}`);
return (
this.state.loaderState ? (
<View style={[styles.container]}>
<ActivityIndicator style={styles.spinner} animating={true} size={60} color='#fff'/>
</View>) : (<View/>)
)
}
}
And part of my parent component (here Rides) which send the updated data from its state to child's(here LazyLoader) prop:
export default class Rides extends React.Component {
constructor(props){
super(props);
this.handleRidePress = this.handleRidePress.bind(this);
this.navigateToRideDetails = this.navigateToRideDetails.bind(this);
this.state = {
flash: true
};
}
handleRidePress(rideId){
this.setState({
flash: true
}, () => {
console.log(`Begining handleRidePress for rideId: ${rideId}`);
if(doSomeTimeConsumingOperation){
// Time consuming operation goes here
}
this.navigateToRideDetails({rideId: rideId, pingData:pingData.slice(), rideData: rideDetails});
});
}
navigateToRideDetails(rideObj){
this.setState({
flash: false
}, () => {console.log(`navigateToRideDetails setState cb`); this.props.navigation.navigate('RideLog', rideObj);});
}
render(){
console.log(`flash: ${this.state.flash}`);
return(
<Gradient.FullGradient>
<Reusables.LazyLoader loaderState={this.state.flash}/>
<PRides rides={this.state.rideTiles} onDeletePress={this.deleteRide} navigation={this.props.navigation} onRideDetailPress={this.handleRidePress}/>
</Gradient.FullGradient>
)
}
}
When handleRidePress function is called it updates the flash state using setState() but child component doesn't get rerender. I tried using shouldComponentUpdate() in both component and returned true by default, but It doesn't work.
I think error is in the way you are comparing two objects here:
if(this.props != nextProps) {
....
}
This is not the correct way to check whether two objects are same or not, check the values:
if(this.props.loaderState != nextProps.loaderState) {
....
}
Check this answer How to determine equality for two JavaScript objects?
Check this snippet:
let a = {b: true};
let b = {b: true};
console.log(a == b); //false
I modified navigateToRideDetails to execute setState independently as react batches state updates that occur in event handlers and lifecycle methods. Thus, if we are updating state multiple times in a function handler, React will wait for event handling to finish before re-rendering. In my case, the flash value in state was getting reset after operation gets completed. Hence ActivityIndicator was not getting displayed.
Here is modified navigateToRideDetails function
navigateToRideDetails(rideObj){
setTimeout(() => {
this.setState({
flash: false
}, ()=>{
this.props.navigation.navigate('RideLog', rideObj);
});
}, 0);
}
This solves the issue :)

Resources