I'm trying to make a chronometer component who hides himself after a delay. It works but I have a warning when the Chronometer disappear and I don't know how to deal with it.
Warning: Cannot update a component (WorkoutScreen
) while rendering a different component (Chronometer
). To locate the bad setState() call inside Chronometer
WorkoutScreen.tsx
const WorkoutScreen = ({
navigation,
route,
}: RootStackScreenProps<"Workout">) => {
const [inRest, setInRest] = useState(false)
const [restTime, setRestTime] = useState(5)
//I pass it to child
const handleEndRestTime = () => {
setInRest(false)
}
//
return (
<Layout style={styles.container}>
<Button
onPress={() => {
setInRest(!inRest)
}}
>
Trigger chronometer
</Button>
{inRest && (
<Chronometer onEnd={handleEndRestTime} seconds={restTime}></Chronometer>
)}
</Layout>
)
}
Chronometer.tsx
const Chronometer = ({ seconds, onEnd }: Props) => {
const [timer, setTimer] = useState<number>(seconds)
const [pause, setPause] = useState(false)
const [running, setRunning] = useState(true)
useEffect(() => {
let interval: NodeJS.Timer
if (pause === true || running === false) {
;() => clearInterval(interval)
} else {
interval = setInterval(() => {
setTimer((timer) => timer - 1)
}, 1000)
}
return () => {
clearInterval(interval)
}
}, [pause, running])
if (timer === 0 && running === true) {
setRunning(false)
//From parent
onEnd()
//
}
return (
<View style={styles.container}>
<View style={styles.chronometer}>
<View style={styles.controls}>
<Text>{formatHhMmSs(timer)}</Text>
</View>
<Button
onPress={() => {
setPause(!pause)
}}
>
Pause
</Button>
</View>
</View>
)
}
When I remove the "{inRest && " the warning disappear.
In the future I want that the User can retrigger Chronometer as he want
Thanks in advance !
CodePudding user response:
There are two state updates that happen simultaneously and conflict with React rendering UI reconciliation
setRunning(false)
inside the Chronometer component will rerender this component when the timer ends.setInRest(false)
inside WorkoutScreen component will also rerender when the timer ends.
Both those rerenders happen at the same timer and WorkoutScreen rerender is triggered by the child component.
The solution is to avoid triggering state change inside the parent component caused by the child component.
const WorkoutScreen = ({
navigation,
route,
}: RootStackScreenProps<"Workout">) => {
const [restTime, setRestTime] = useState(5);
//I pass it to child
const handleEndRestTime = () => {
// Handle logic when workout time end
};
//
return (
<Layout style={styles.container}>
<Chronometer onEnd={handleEndRestTime} seconds={restTime}></Chronometer>
</Layout>
);
};
const Chronometer = ({ seconds, onEnd }: Props) => {
const [timer, setTimer] = useState < number > seconds;
const [pause, setPause] = useState(false);
const [running, setRunning] = useState(true);
const [inRest, setInRest] = useState(false);
useEffect(() => {
let interval: NodeJS.Timer;
if (pause === true || running === false) {
() => clearInterval(interval);
} else {
interval = setInterval(() => {
setTimer((timer) => timer - 1);
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [pause, running]);
if (timer === 0 && running === true) {
setRunning(false);
setInRest(false);
//From parent
onEnd();
//
}
return (
<View style={styles.container}>
<Button
onPress={() => {
setInRest(!inRest);
}}
>
Trigger chronometer
</Button>
{inRest && (
<View style={styles.chronometer}>
{/* Show Timer counter only when is running */}
{running && (
<View style={styles.controls}>
<Text>{formatHhMmSs(timer)}</Text>
</View>
)}
<Button
onPress={() => {
setPause(!pause);
}}
>
{running ? "Pause" : "Start"}
</Button>
</View>
)}
</View>
);
};