tldr; How do I simulate componentDidUpdate
or otherwise use the key
prop with an array to force my component to be reset?
I'm implementing a component which displays a timer and executes a callback when it reaches zero. The intent is for the callback to update a list of objects. The latter component is made of the new React hooks useState
and useEffect
.
The state
contains a reference to the time at which the timer was started, and the time remaining. The effect
sets an interval called every second to update the time remaining, and to check whether the callback should be called.
The component is not meant to reschedule a timer, or keep the interval going when it reaches zero, it's supposed to execute the callback and idle. In order for the timer to refresh, I was hoping to pass an array to key
which would cause the component's state to be reset, and thus the timer would restart. Unfortunately key
must be used with a string, and therefore whether or not my array's reference has changed produces no effect.
I also tried to push changes to the props by passing the array that I was concerned about, but the state was maintained and thus the interval was not reset.
What would be the preferred method to observe shallow changes in an array in order to force a state to be updated solely using the new hooks API?
import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; function getTimeRemaining(startedAt, delay) { const now = new Date(); const end = new Date(startedAt.getTime() + delay); return Math.max(0, end.getTime() - now.getTime()); } function RefresherTimer(props) { const [startedAt, setStartedAt] = useState(new Date()); const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(startedAt, props.delay)); useEffect(() => { if (timeRemaining <= 0) { // The component is set to idle, we do not set the interval. return; } // Set the interval to refresh the component every second. const i = setInterval(() => { const nowRemaining = getTimeRemaining(startedAt, props.delay); setTimeRemaining(nowRemaining); if (nowRemaining <= 0) { props.callback(); clearInterval(i); } }, 1000); return () => { clearInterval(i); }; }); let message = `Refreshing in ${Math.ceil(timeRemaining / 1000)}s.`; if (timeRemaining <= 0) { message = 'Refreshing now...'; } return <div>{message}</div>; } RefresherTimer.propTypes = { callback: PropTypes.func.isRequired, delay: PropTypes.number }; RefresherTimer.defaultProps = { delay: 2000 }; export default RefresherTimer;
Attempted to use with key
:
<RefresherTimer delay={20000} callback={props.updateListOfObjects} key={listOfObjects} />
Attempted to use with a props change:
<RefresherTimer delay={20000} callback={props.updateListOfObjects} somethingThatChanges={listOfObjects} />
listOfObjects
refers to an array of objects, where the objects themselves won't necessarily change, so the array should be compared with !==
. Typically, the value will be coming from Redux
, where the action updateListOfObjects
causes the array to be reinitialised like so: newListOfObjects = [...listOfObjects]
.
ComponentDidUpdate is a React component lifecycle method invoked immediately after a component's updates are flushed to the DOM. This is one of the most used built-in methods, which is not called for the initial render nor applicable to your functional details.
useEffect can only be used in a React functional component. componentDidUpdate can only be called within a class component.
React Hooks Equivalent of componentDidUpdate The equivalent of the componentDidUpdate lifecycle method is also the useEffect hook.
You can hack the useMemo hook to imitate a componentWillMount lifecycle event. Just do: const Component = () => { useMemo(() => { // componentWillMount events },[]); useEffect(() => { // componentDidMount events return () => { // componentWillUnmount events } }, []); };
The useRef
creates an "instance variable" in functional component. It acts as a flag to indicate whether it is in mount or update phase without updating state.
const mounted = useRef(); useEffect(() => { if (!mounted.current) { // do componentDidMount logic mounted.current = true; } else { // do componentDidUpdate logic } });
In short, you want to reset your timer when the reference of the array changes, right ? If so, you will need to use some diffing mechanism, a pure hooks based solution would take advantage of the second parameter of useEffect
, like so:
function RefresherTimer(props) { const [startedAt, setStartedAt] = useState(new Date()); const [timeRemaining, setTimeRemaining] = useState(getTimeRemaining(startedAt, props.delay)); //reset part, lets just set startedAt to now useEffect(() => setStartedAt(new Date()), //important part [props.listOfObjects] // <= means: run this effect only if any variable // in that array is different from the last run ) useEffect(() => { // everything with intervals, and the render }) }
More information about this behaviour here https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With