Reputation: 65
I'm learning React and I have problem with refreshing a child component when parent's state is changed by another child. I found that I should use componentWillReceiveProps()
and it works but with one click delay. What should I change to get immediate update?
I've posted my code on CodePen to make it easier to explain. If it's better to post it here directly please let me know, and I will update.
Problems:
Is it possible to make it only with React or should I start learning Redux?
Edit: My code
import React from 'react';
import './PomodoroClock.scss';
class PomodoroClock extends React.Component {
constructor(props) {
super(props);
this.state = {
sessionLength: 25,
breakLength: 5,
};
this.handleTime = this.handleTime.bind(this);
this.handleReset = this.handleReset.bind(this);
}
handleTime(type, time) {
this.setState({
[type]: time
});
}
handleReset() {
this.setState({
sessionLength: 25,
breakLength: 5,
});
}
render() {
return (
<div className="container">
<ClockSetter clockType="break" initialLength={this.state.breakLength} handleTime={this.handleTime} />
<ClockSetter clockType="session" initialLength={this.state.sessionLength} handleTime={this.handleTime} />
<Timer sessionLength={this.state.sessionLength} reset={this.handleReset}/>
<span>TEST: State session - {this.state.sessionLength} State break - {this.state.breakLength}</span>
</div>
);
}
}
class ClockSetter extends React.Component {
constructor(props) {
super(props);
this.state = {
length: this.props.initialLength
}
this.handleDecrease = this.handleDecrease.bind(this);
this.handleIncrease = this.handleIncrease.bind(this);
this.refresh = this.refresh.bind(this);
}
handleDecrease() {
if(this.state.length > 1) {
this.setState ({
length: this.state.length - 1
});
}
this.props.handleTime(this.props.clockType+'Length', this.state.length - 1);
}
handleIncrease() {
if(this.state.length < 60) {
this.setState ({
length: this.state.length + 1
});
}
this.props.handleTime(this.props.clockType+'Length', this.state.length + 1);
}
refresh() {
this.setState({
length: this.props.initialLength
});
}
componentWillReceiveProps(props) {
if(this.state.length !== this.props.initialLength) {
this.refresh();
}
}
render() {
let type = this.props.clockType;
return(
<div className="clock-setter">
<div id={type + '-label'} className="first-letter-capitalize">{type + ' Length'}</div>
<span id={type + '-decrement'} className="button" onClick={this.handleDecrease}>-</span>
<span id={type + '-length'}>{this.state.length}</span>
<span id={type + '-increment'} className="button" onClick={this.handleIncrease}>+</span>
</div>
);
}
}
class Timer extends React.Component {
constructor(props) {
super(props);
this.state = {
activeCountdown: 'Session',
length: this.props.sessionLength
}
this.refresh = this.refresh.bind(this);
}
refresh() {
this.setState({
length: this.props.sessionLength
});
}
componentWillReceiveProps(props) {
if(this.state.length !== this.props.sessionLength) {
this.refresh();
}
}
render() {
return(
<div className="timer">
<span id="timer-label">{this.state.activeCountdown}</span>
<div id="time-left">{this.state.length}</div>
<span id="start_stop" className="button">Start/Stop</span>
<span id="reset" className="button" onClick={this.props.reset}>Reset</span>
</div>
);
}
}
export default PomodoroClock;
Upvotes: 3
Views: 1729
Reputation: 20624
Let's refactor your code in such a way that fixes the immediate issue while at the same time addresses some bad practices and antipatterns that will cause you headaches moving forward.
And because you're just starting, this is the perfect time to learn about hooks because they will make your code much more simple and easier to follow.
The main pitfall in your code is duplication of state. Let's start with the Timer component.
You are setting its initial state length
to be the value of its parents state sessionLength
. Even though you can perceive this to be some type of "initial" state and then afterwards the Timer's length
will be independent of sessionLength
once the countdown starts, this is not necessary. In fact.. duplication of state is not necessary in 99% of situations.
So what state should the Timer have? I would reckon that the timer might have its own internal counter state such that you display the current time like this.props.sessionLength - this.state.elapsedTime
, but in your case the Timer isn't actually doing any timing. You're keeping track of the current time at the parent level anyways.
Knowing this.. what should the Timer state be? Nothing! The answer is no state. Timer can be a function, not a class, and receive props and display them.
function Timer(props) {
return (
<div className="timer">
<span id="timer-label">Session</span>
<div id="time-left">{props.sessionLength}</div>
<span id="start_stop" className="button">
Start/Stop
</span>
<span id="reset" className="button" onClick={props.reset}>
Reset
</span>
</div>
)
}
If that's all you change, this already solves your question.
Next let's look at the ClockSetter component. You are duplicating the state here in the exact same way, and not only that, you have extra handlers which simply call the parents handler handleTime
, introducing extra steps and complexity which add unnecessary noise to your application. Let's get rid of the state and the extra handlers altogether, and as such we can use a function again, instead of a class:
function ClockSetter(props) {
let type = props.clockType
return (
<div className="clock-setter">
<div id={type + '-label'} className="first-letter-capitalize">
{type + ' Length'}
</div>
<span
id={type + '-decrement'}
className="button"
onClick={() => props.handleTime(type + 'Length', props.length - 1)}
>
-
</span>
<span id={type + '-length'}>{props.length}</span>
<span
id={type + '-increment'}
className="button"
onClick={() => props.handleTime(type + 'Length', props.length + 1)}
>
+
</span>
</div>
)
}
I've inlined the onClick
handlers for brevity. You could write the named handleDecrease
and handleIncrease
functions above the return statement and passed them the onClick
if you want. That's just a matter of preference though.
*Note: the prop is now length
not initialLength
. Make sure to update that when rendering your ClockSetter components
For this last refactor I have updated your React cdn to point to the latest stable release 16.8.3
, since it includes hooks.
Instead of using a class, let's write a normal function and use the React.useState
hook. The code looks like this now:
function PomodoroClock() {
let [sessionLength, setSessionLength] = React.useState(25)
let [breakLength, setBreakLength] = React.useState(5)
function handleReset() {
setSessionLength(25)
setBreakLength(5)
}
return (
<div className="container">
<ClockSetter
clockType="break"
length={breakLength}
handleTime={setBreakLength}
/>
<ClockSetter
clockType="session"
length={sessionLength}
handleTime={setSessionLength}
/>
<Timer
sessionLength={sessionLength}
reset={handleReset}
/>
<span>
Parent's state TEST: session - {sessionLength} break -
{breakLength}
</span>
</div>
)
}
and instead of having a single state object with keys that reference each timer, since that was the only way with stateful components before, we call useState
twice each with their respective state and handlers. Now we can remove the type + Length
argument in our ClockSetter component:
onClick={() => props.handleTime(props.length + 1)}
This is the entire program now:
function PomodoroClock() {
let [sessionLength, setSessionLength] = React.useState(25)
let [breakLength, setBreakLength] = React.useState(5)
function handleReset() {
setSessionLength(25)
setBreakLength(5)
}
return (
<div className="container">
<ClockSetter
clockType="break"
length={breakLength}
handleTime={setBreakLength}
/>
<ClockSetter
clockType="session"
length={sessionLength}
handleTime={setSessionLength}
/>
<Timer
sessionLength={sessionLength}
reset={handleReset}
/>
<span>
Parent's state TEST: session - {sessionLength} break -
{breakLength}
</span>
</div>
)
}
function ClockSetter(props) {
let type = props.clockType
return (
<div className="clock-setter">
<div id={type + '-label'} className="first-letter-capitalize">
{type + ' Length'}
</div>
<span
id={type + '-decrement'}
className="button"
onClick={() => props.handleTime(props.length - 1)}
>
-
</span>
<span id={type + '-length'}>{props.length}</span>
<span
id={type + '-increment'}
className="button"
onClick={() => props.handleTime(props.length + 1)}
>
+
</span>
</div>
)
}
function Timer(props) {
return (
<div className="timer">
<span id="timer-label">Session</span>
<div id="time-left">{props.sessionLength}</div>
<span id="start_stop" className="button">
Start/Stop
</span>
<span id="reset" className="button" onClick={props.reset}>
Reset
</span>
</div>
)
}
ReactDOM.render(
<PomodoroClock />,
document.getElementById('root')
);
We've shaved of over 50 lines of code, it's much easier to read, and there's no potential issue of state being duplicated.
Hope that helps and happy coding! Please ask any questions if you need.
Upvotes: 2