I am making a Task application for myself to improve my workflow. I had gotten around to introducing a timer to the tasks which upon reaching 0 sets the task as overdue. Everything looked fine until I realised the timer disappears when the component unmounts and remounts (due to a filtering process I use). I am self teaching myself react and am quite new to it. Everything has been going pretty smooth but I have been stuck on this roadbump for a while now because of how much state is actually handling the Timer.
I will provide you with a brief overview of my code in the simplest way I possibly can as it is quite a lot.
THE INITIAL STRUCTURE OF A TASK.
function createTodo(todo) {
if (!todo) return alert('Please enter a task')
else {
setTodos([...todos,
{id: uuidv4(),
task: todo, // This will be used to display the users input task
description: '', // This will be used to describe the task
catagories: [], // This will be used to filter todos by catagory
completed: false, // This will be used to mark a task as completed or in progress
isEditing: false, // This will be state to determine if a task is being edited
priority: false, // This will be state to determine if a task is being edited
timer: false, // This will be used to set a timer for a task
overdue: false, // This will check whether a timer has run out and a task is overdue
}])
}
}
THE LIST WHICH DISPLAYS THE TASKS
This is where the Tasks are mapped and where the issue is occuring due to the filtering unmounting the components
const filteredTodos = todos.filter(todo => {
// Check if the todo starts with the search term
let matchesSearchTerm = searchTerm === "" || todo.task.toLowerCase().startsWith(searchTerm.toLowerCase());
// Check if the todo matches the checkbox conditions
let matchesPriority = !priorityChecked || todo.priority;
let matchesOverdue = !overdueChecked || todo.overdue;
let matchesCompleted = !completedChecked || todo.completed;
// Return true if all conditions are met
return matchesSearchTerm && matchesPriority && matchesCompleted && matchesOverdue;
});
{filteredTodos.length > 0 ? (
filteredTodos.map((todo, index) => (
<TodoItem
key={index}
todo={todo}
editTodo={editTodo}
editingItemId={editingItemId}
editDescription={editDescription}
editTitle={editTitle}
deleteTodo={deleteTodo}
changePriority={changePriority}
markComplete={markComplete}
createTimer={createTimer}
changeOverdue={changeOverdue}
updateTimer={updateTimer}
/>
))
THE TODO ITEM COMPONENT IS IRRELEVANT LETS JUST ASSUME ITS THIS
<div>
<Timer
todo={todo}
timerActive={timerActive}
setTimerActive={setTimerActive}
createTimer={createTimer}
changeOverdue={changeOverdue}
updateTimer={updateTimer}
/>
</div>
THIS IS THE TIMER COMPONENT ITSELF
import CountdownTimer from './CountdownTimer';
export default function Timer({ todo, createTimer, timerActive, setTimerActive, changeOverdue, updateTimer}) {
// Timer Parameters
const timerMinute = 60;
const timerHour = 3600;
const timerDay = 86400;
const timerWeek = 604800;
// Timer
const [timerInput, setTimerInput] = useState(0)
const [timeLeft, setTimeLeft] = useState(0);
const [timerType, setTimerType] = useState(timerMinute);
const [timerMenu, setTimerMenu] = useState(false);
const [userActivatedTimer, setUserActivatedTimer] = useState(false);
const selectedTimeType = timerType === timerMinute ? "Minutes" : timerType === timerHour ? "Hours" : timerType === timerDay ? "Days" : "Weeks";
useEffect(() => {
let timer = null;
if (timerActive && timeLeft > 0) {
timer = setInterval(() => {
setTimeLeft(timeLeft => timeLeft - 1);
updateTimer(todo.id, timeLeft - 1);
}, 1000);
} else {
clearInterval(timer);
}
return () => clearInterval(timer);
}, [timerActive, timeLeft]);
return (
<div className="flex flex-col relative justify-center items-center bg-zinc-800 rounded-lg p-2">
{timerActive ? (
<>
<p className='text-white p-1'>TASK TIMER</p>
<CountdownTimer
todo={todo}
timeLeft={timeLeft}
userActivatedTimer={userActivatedTimer}
changeOverdue={changeOverdue} />
</>
) : ( THIS IS AN INPUT FOR THE USER TO START THE TIMER IF NOT STARTED ))
FINAL COMPONENT WHICH IS THE COUNTDOWN TIMER THAT IS USED TO DISPLAY THE TIMER CONVERTED INTO ACTUAL TIME FOR THE USER
import React, { useEffect, useState } from 'react';
const CountdownTimer = ({todo, timeLeft, userActivatedTimer, changeOverdue }) => {
const [time, setTime] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
useEffect(() => {
const intervalId = setInterval(() => {
if (timeLeft <= 0) {
if (userActivatedTimer) {
changeOverdue(todo.id);
}
clearInterval(intervalId);
} else {
timeLeft--;
const seconds = (timeLeft % 60).toString().padStart(2, '0');
const minutes = Math.floor((timeLeft / 60) % 60).toString().padStart(2, '0');
const hours = Math.floor((timeLeft / 3600) % 24).toString().padStart(2, '0');
const days = Math.floor((timeLeft / 86400)).toString().padStart(2, '0');
setTime({
days,
hours,
minutes,
seconds,
});
}
}, 1000);
return () => clearInterval(intervalId);
}, [timeLeft]);
return (
<div className='bg-amber-500 p-2 rounded-lg'>
<p className='text-white'>{time.days}:{time.hours}:{time.minutes}:{time.seconds}</p>
</div>
);
};
export default CountdownTimer;
Essentially everything works as intended until I unmount the todoItem, as this resets the timer in the TodoItem. So I would like to make it so that the TodoItem retains its timer property even on unmounting (For some reason all the other data from the todoItem remains, its just the timer which is reset.) I have read into this quite a lot and there have been a lot of suggestions to use Context or Redux, instead of just prop drilling by raising the timer states, I dont know much at the moment about Context or Redux as I haven’t needed to use it yet. So I am essentially looking for an expert opinion on how to handle a situation like this. I don’t expect or need a full solution as it is too much code to evaluate just a nudge in the right direction of how to handle this issue. (Also bare in mind that I have multiple tasks with their own individual timers)