I am writing a simple timer app in react. The timer is not owned by the react component, but will be shared across several timer components. As part of component mounting, I register the react component with my universal timer, and it updates the component with 'ticks'.
My expectation is that when the component is mounted, I register it with my master timer and it starts receiving ticks. When I unmount the component, it is deregistered. This works perfectly when using a react class component (as shown in the code below).
However, when I try to do this with functional components and useEffect, I can't get the expected behavior. I've listed in the code below two approaches for useEffect, and neither does exactly what I want. I have explained my issues with both in the code below.
Obviously I'd like to be using the latest from react (function components), but I haven't gotten them to work right in this instance.
import * as React from 'react';
import { register, deregister, formatMsToTime } from '../logic/Timer';
export default class Timer extends React.Component {
constructor(props) {
super(props);
const { name, initialTime } = props;
this.name = name;
this.state = {
time: initialTime,
};
}
componentDidMount() {
register(this.name, (elapsed) => this.setState(state => ({ time: state.time - elapsed })));
}
componentDidUnmount() {
deregister(this.name);
}
render() {
return (<>
{formatMsToTime(this.state.time)}
</>
);
}
}
export default function Timer(props) {
const [time, setTime] = useState(props.initialTime);
//useEffect option #1
/*
This technically works, the timer is updated properly and shows the countdown I'm expecting.
However, this calls register/deregister with every render of the component, which I don't want.
Render is called with every tick of the timer, which happens every 50ms (so, a lot).
*/
useEffect(() => {
register(props.name, (elapsed) => setTime(time - elapsed));
return () => {
deregister(props.name);
}
})
//useEffect option #2
/*
This works for about .1 seconds. On the first render, setTime refers to a function used for setting the state, and it gets called with the elapsed time.
On the second render, the state variable time and its setter are overwritten, and 'setTime' in useEffect no longer refers to the correct setTime function.
*/
useEffect(() => {
register(props.name, (elapsed) => setTime(time - elapsed));
return () => {
deregister(props.name);
}
}, [])
return (<>
{formatMsToTime(time)}
</>);
}
CodePudding user response:
You might need the functional updater method
useEffect(() => {
register(props.name, (elapsed) => setTime(t => t - elapsed));
// replace setTime(time - elapsed)) with setTime(t => t - elapsed)
return () => {
deregister(props.name);
}
}, []) // looks like props.name dependency need to be added
Hope it helps, I encourage you to go through then new React docs, they will help a lot, cheers
Edit: link to functional updater method which is more relevant to this answer, Thanks @Michael for including it in the comment to improve the answer