Timer does not countdown in ReactJS - javascript

I am building a simple stopwatch app in ReactJS (count down to 0).
So far I have an input where the user can set how long the timer should be and a button which updates the timer state with the input data.
However I am not able to have the timer countdown, even with using setInterval().
App.jsx
import React, {Component} from 'react';
import Timer from './timer';
import './app.css';
import { Form, FormControl, Button} from 'react-bootstrap';
export class App extends Component {
constructor(props) {
super(props);
this.state = {
timerPosition: '',
newTimerPosition: '',
};
}
startTimer() {
this.setState({timerPosition:this.state.newTimerPosition})
}
render() {
return (
<div className="App-title"> Stopwatch </div>
<Timer timerPosition={this.state.timerPosition} />
<Form inline>
<FormControl
className="Deadline-input"
onChange={event => this.setState({newTimerPosition: event.target.value})}
placeholder='Set timer'/>
<Button onClick={() => this.startTimer()}>Set new timer</Button>
</Form>
</div>
)}
}
export default App;
Timer.jsx
import React, {Component} from 'react';
import './app.css';
export class Timer extends Component {
constructor(props) {
super(props);
this.state = {
secondsRemaining: ''
}
}
componentWillMount(){
this.startTimer(this.props.timerPosition);
}
componentDidMount(){
setInterval(() => this.startTimer(this.props.timerPosition), 1000);
}
startTimer(timerCallback) {
this.setState({secondsRemaining: timerCallback --})
}
render() {
return (
<div>
<div></div>
{this.state.secondsRemaining} seconds remaining
</div>
)}
}
export default Timer;
The timer does not decrement every second and just stays at the original position.

The primary issue is in the interval callback in Timer's componentDidMount:
componentDidMount(){
setInterval(() => this.startTimer(this.props.timerPosition), 1000);
}
Note that you're constantly reusing this.props.timerPosition as the timer position, instead of using the current state. So you're setting the state back to the initial state from the props.
Instead, you want to use the current state. How you use that state will depend on how you want the timer to behave, but beware of two things:
Never call this.setState({foo: this.state.foo - 1}) or similar. State updates are asynchronous and can be combined. Instead, pass a callback to setState.
Nothing is guaranteed to happen at any particular time or on a specific interval, so don't just decrement your counter; instead, see how long it's been since you started, and use that information to subtract from your initial counter value. That way, if your timer is delayed, you still show the correct value.

The problem is you keep using the same prop value secondsRemaining. You decrement the same value in the propthis.props.timerPosition each time. So the state value secondsRemaining never changes.
Instead decrement the secondsRemaining value from state using the setState method which takes a callback. The first parameter of the callback is the current state. so you do:
this.setState((prevState, props) => {
return { secondsRemaining: prevState.secondsRemaining - 1 };
});
You also need to set your initial state value to the supplied props value:
componentWillMount(){
this.setState({ secondsRemaining: this.props.timerPosition });
... other code
}

Related

Component should return same value wherever it's called. and Main Component should not rerender when Sub Component useState is updating in react?

I want same value in Home function from Component and Home function should not rerender when Component useState is updating.
import { useState, useEffect } from "react";
function Component() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log("rerender")
})
useEffect(() => {
let interval = setInterval(() => {
setCount(Math.floor(Math.random() * 1000))
}, 1000)
return () => clearInterval(interval)
}, [])
return count
}
function OtherStuff() {
console.log("OtherStuff")
return (<h1>Should Not Rerender OtherStuff</h1>)
}
function MoreStuff() {
console.log("MoreStuff")
return (<h1>Should Not Rerender MoreStuff</h1>)
}
export default function Home() {
return (
<>
<h1>Hello</h1>
<h1>
<Component />
</h1>
<OtherStuff />
<MoreStuff />
<h1>
<Component />
</h1>
</>
);
}
I have called Component function two times, I want same value should render to both from Component.
and when ever Component {count} is update by using setCount, Main function should not rerender. at this time, there is no rerender for Component in Main function which is OK.
this is the simple problem.
Thanks.
Each instance of a component has its own state. Meaning their states are not shared. If you wanna share the state between them, one way would be to lift the state up.
If you do not want the Home to rerender, you cannot move the state to Home. So you probably need another component which holds the state. Because any component which has a react useState hook, will be rerendered when the state is updated.
You can use react context to do so. Wrap your instances of Component inside a context provider which holds the count state and provides count and setCount.
First you need to create a context:
const CountContext = createContext();
Then create a component which provides the context and holds the state:
const CountProvider = (props) => {
// `useCounter` uses `useState` inside.
const [count, setCount] = useCounter();
return (
<CountContext.Provider value={{ count, setCount }}>
{props.children}
</CountContext.Provider>
);
};
(OP provided a codesandbox which uses useCounter custom hook. It stores a number to count state and updates it to a random number every 1000ms.)
Then wrap it around your components:
function Parent() {
return (
<div>
<CountProvider>
<Counter />
<Counter />
</CountProvider>
</div>
);
}
codesandbox
Note that:
If you render another component inside CountProvider using children prop, it will NOT rerender every time count state updates, unless it uses the context.
But if you render a component inside CountProvider and render it in the function, it WILL rerender, unless you use memo.

Functional component definition inside class component's render() method, state resets when created through JSX, but not when called directly

If I define a functional component inside of a class component's render() method, then the component's state is getting reset every time the class component's render() method is called. If I call the functional component directly though, the state does not reset.
Look at the following example:
import React from 'react';
import Counter from './Counter'
const MilliCounter = ({name}) => {
return <Counter name={name} initial={1e6} />
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
flag: false
}
}
onButtonClick = (event) => {
this.setState({flag: !this.state.flag});
};
render() {
const HundoCounter = ({name}) => {
return <Counter name={name} initial={100} />
};
return (<div>
<button onClick={this.onButtonClick}>Change State</button>
<div>{`Flag: ${this.state.flag}`}</div>
<HundoCounter name="Component Def Inside render() - Hundo JSX"/>
{HundoCounter({name: 'Component Def Inside render() - Hundo Function Call'})}
<MilliCounter name="Component Def Outside render() - Milli JSX"/>
{MilliCounter({name: 'Component Def Outside render() - Milli Function Call'})}
</div>)
}
}
export default App;
import * as React from 'react'
export default class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: props.initial
}
}
onButtonClick = (event) => {
this.setState({
count: this.state.count + 1
})
};
render() {
return (
<div style={{border: '1px solid black', margin: '1rem', padding: '0.67rem'}}>
<h6>{this.props.name}</h6>
<p>Count: {this.state.count}</p>
<button onClick={this.onButtonClick}>Click Me</button>
</div>
)
}
}
Here's a video showing the demo app in action.
https://i.imgur.com/WfS8DXJ.mp4
As you can see, when the button is clicked it changes the flag to true which forces a re-render. During this the state of the functional component HundoCounter defined with JSX is reset, but not the one that is called directly.
It makes sense to me that the state would reset, because it's creating a new definition of HundoCounter every time render() is called. How come the state for the HundoCounter that's called directly as a function does not get reset?
I believe the reason is because you're re-rendering the parent component, which then resets the initial={100} to set it back to 100, when the child component is re-rendered due to the parent re-render.
Which is the intended behaviour
As for why the second one isn't resetting i don't know, but it seems odd that it is not, since it's value should also be reset
Okay it seems odd. I think it is related with React's reconciliation and diff algorithm. When I add the key property to Counter component it behaves what we expect.
const HundoCounter = ({ name }) => {
console.log("hundo function")
return <Counter key={Math.random()} name={name} initial={100} />
};
I think render() method is called and the diff algorithm recurses on the previous result and the new result and somehow function surround the component and behaves like it is the same component. Btw I like this experiment :)

React Router v4 previous state component

I'm looking for a solution - react router v4 doesn't hold the previous state of component. For example, here is a simple counter:
import React, { Component } from "react";
class Schedule extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
};
this.holdState = this.holdState.bind(this);
}
holdState() {
this.props.location.state = this.state.counter;
const state = this.state;
this.setState({
counter: state.counter + 1
});
}
render() {
return (
<div>
<ul>
<li>6/5 # Evergreens</li>
<li>6/8 vs Kickers</li>
<li>6/14 # United</li>
</ul>
<div>
<button onClick={() => this.holdState()}>Click</button>
<span>{this.state.counter}</span>
</div>
</div>
);
}
}
export default Schedule;
I was trying to push the state into location and history by props.
But whenever I press "browser button back" from the next page, it always resets the state.
Can someone tell me where I'm doing a mistake?
Whenever your component mounts, your constructor will be initiated which will reset the state back to 0. This happens when you are on another component and press back button in which case your current Route gets mounted.
Also directly mutating the location.state value is not a right approach. What you need is to save your value in localStorage and refill it in state if it exists.
Another thing is that when you wish to update the state based on prevState, you could make use of functional setState. Check this for more details:
When to use functional setState
import React, { Component } from "react";
class Schedule extends Component {
constructor(props) {
super(props);
this.state = {
counter: localStorage.getItem('counter') || 0
};
this.holdState = this.holdState.bind(this);
}
holdState() {
localStorage.setItem('counter', this.state.counter);
this.setState(prevState => ({
counter: prevState.counter + 1
}));
}
render() {
return (
<div>
<ul>
<li>6/5 # Evergreens</li>
<li>6/8 vs Kickers</li>
<li>6/14 # United</li>
</ul>
<div>
<button onClick={() => this.holdState()}>Click</button>
<span>{this.state.counter}</span>
</div>
</div>
);
}
}
export default Schedule;
I was trying to push state into location and history by props. BBut whenever I > press "browser button back" from the next page, it always resets the state.
when you call setState you're not changing the route, you're just triggering a rerender of your component.
If you press the back button after incremented the counter react router will symply pop the last route from the history stack but since you don't have a previous route the component will be remount hence the resetted state.
To implement what i suppose you want to achieve you need to explicitely change the route every setstate (e.g. adding a parameter in the query string with the current value of the counter, like ?counter=1, ?counter=2..) , this way you'll be sure that a new route will be push on top of the stack every setState and the back button will then work as you expect.

Uncaught RangeError Maximum call stack size exceeded in React App

I'm learning React and for training, I want to create a basic Todo app. For the first step, I want to create a component called AddTodo that renders an input field and a button and every time I enter something in the input field and press the button, I want to pass the value of the input field to another component called TodoList and append it to the list.
The problem is when I launch the app, the AddTodo component renders successfully but when I enter something and press the button, the app stops responding for 2 seconds and after that, I get this: Uncaught RangeError: Maximum call stack size exceeded and nothing happens.
My app source code: Main.jsx
import React, {Component} from 'react';
import TodoList from 'TodoList';
import AddTodo from 'AddTodo';
class Main extends Component {
constructor(props) {
super(props);
this.setNewTodo = this.setNewTodo.bind(this);
this.state = {
newTodo: ''
};
}
setNewTodo(todo) {
this.setState({
newTodo: todo
});
}
render() {
var {newTodo} = this.state;
return (
<div>
<TodoList addToList={newTodo} />
<AddTodo setTodo={this.setNewTodo}/>
</div>
);
}
}
export default Main;
AddTodo.jsx
import React, {Component} from 'react';
class AddTodo extends Component {
constructor(props) {
super(props);
this.handleNewTodo = this.handleNewTodo.bind(this);
}
handleNewTodo() {
var todo = this.refs.todo.value;
this.refs.todo.value = '';
if (todo) {
this.props.setTodo(todo);
}
}
render() {
return (
<div>
<input type="text" ref="todo" />
<button onClick={this.handleNewTodo}>Add to Todo List</button>
</div>
);
}
}
AddTodo.propTypes = {
setTodo: React.PropTypes.func.isRequired
};
export default AddTodo;
TodoList.jsx
import React, {Component} from 'react';
class TodoList extends Component {
constructor(props) {
super(props);
this.renderItems = this.renderItems.bind(this);
this.state = {
todos: []
};
}
componentDidUpdate() {
var newTodo = this.props.addToList;
var todos = this.state.todos;
todos = todos.concat(newTodo);
this.setState({
todos: todos
});
}
renderItems() {
var todos = this.state.todos;
todos.map((item) => {
<h4>{item}</h4>
});
}
render() {
return (
<div>
{this.renderItems()}
</div>
);
}
}
export default TodoList;
First time componentDidUpdate is called (which happens after first change in its props/state, which in your case happens after adding first todo) it adds this.props.addToList to this.state.todo and updates state. Updating state will run componentDidUpdate again and it adds the value of this.props.addToList to 'this.state.todo` again and it goes infinitely.
You can fix it with some dirty hacks but your approach is a bad approach overall. Right thing to do is to keep todos in parent component (Main), append the new todo to it in setNewTodo (you may probably rename it to addTodo) and pass the todos list from Main state to TodoList: <TodoList todos={this.state.todos}/> for example.
The basic idea of react is whenever you call setState function, react component get updated which causes the function componentDidUpdate to be called again when the component is updated.
Now problem here is you are calling setState function inside componentDidUpdate which causes the component to update again and this chain goes on forever. And every time componentDidUpdate is called it concat a value to the todo. So a time come when the memory gets full and it throws an error. You should not call setState function inside functions like componentWillUpdate,componentDidUpdate etc.
One solution can be to use componentWillReceiveProps instead of componentDidUpdate function like this:
componentDidUpdate(nextProps) {
var newTodo = nextProps.addToList;
this.setState(prevState => ({
todos: prevState.todos.concat(newTodo)
}));
}

setState in ReactJS

I'm new to ReactJS and I can't seem to find out why the result of the following setState is not as I expect it to be (i.e. to increment the value every second by 1)
import React from 'react';
import ReactDOM from 'react-dom';
class Layout extends React.Component {
constructor() {
super();
this.state = {
name: "Behnam",
i: 0
}
}
render() {
setInterval(() => {
this.setState({ name : "Behnam" + this.state.i });
this.setState({ i: this.state.i + 1 });
}, 1000);
return (
<div className="container">
{this.state.name}
</div>
);
}
}
ReactDOM.render(<Layout />, document.getElementById('app'));
Instead the output string rapidly increases (I guess as fast as react is trying to keep its' virtual DOM updated). So I was wondering what is the right way to do this?
Every time you change the state, you rerender the component.
Because you initiated the setInterval in the render method, you get another interval, which changes the state, and rerenders, and so on.
Move the setInterval to componentDidMount, which is invoked only once, when the component mounts:
import React from 'react';
import ReactDOM from 'react-dom';
class Layout extends React.Component {
constructor() {
super();
this.state = {
name: "Behnam",
i: 0
}
}
componentDidMount() { set the interval after the component mounted, and save the reference
this.interval = setInterval(() => {
this.setState({
name: `Behnam${this.state.i}`,
i: this.state.i + 1
});
}, 1000);
}
componentWillUnmount() {
this.interval && clearInterval(this.interval); // clear the interval when the component unmounts
}
render() {
return (
<div className="container">
{this.state.name}
</div>
);
}
}
ReactDOM.render(<Layout />, document.getElementById('app'));
Currently, it is creating an interval every time the component is rendered, so there are multiple timers incrementing the value. You probably want to do it in componentDidMount() instead of render(). See docs.
import React from 'react';
import ReactDOM from 'react-dom';
class Layout extends React.Component {
constructor() {
super();
this.state = {
name: "Behnam",
i: 0
}
}
componentDidMount() {
setInterval(() => {
this.setState({ name : "Behnam" + this.state.i });
this.setState({ i: this.state.i + 1 });
}, 1000);
}
render() {
return (
<div className="container">
{this.state.name}
</div>
);
}
}
ReactDOM.render(<Layout />, document.getElementById('app'));
Every time a render is triggered, you're calling setInterval again, adding to the number of active intervals on the page.
You should perhaps make use of another lifecycle method, such as componentDidMount. You should remember to save the interval ID returned by setInterval, so that you can call clearInterval in componentWillUnmount.

Categories

Resources