Build a Task Tracking App
Course: React JS & Tailwind CSS - Full Course
Congratulations on making it this far! You’ve learned a lot so far: components, props, state, conditional rendering, forms, two-way binding, and side effects with useEffect.
Now it's time to put it all together and finish building your Task Tracker App!
So far the Task Tracker App can do the following:
- Add Tasks
- Display Tasks
- Edit Tasks
- Delete Tasks
- Mark Tasks Complete or Incomplete
What still needs to be done:
- Persist the tasks with
localStorage
- Filter tasks based on completion status
- Deploy app
Try to give it a shot before looking at the solution!
Solution
With the core functionality of our app in place, we now need to persist our tasks so that they still show up when we refresh or close the browser.
Loading Tasks From localStorage
Let's set up a useEffect
to check if there are any tasks in localStorage:
useEffect(() => {
const storedTasks = localStorage.getItem("tasks")
if (storedTasks) {
try {
setTaskList(JSON.parse(storedTasks));
} catch (error) {
console.error("Failed to parse tasks from localStorage", error)
}
}
}, [])
We're using a try catch block here in case the data's corrupt. tasks
are stored as JSON string so we need to parse it to convert it back into an object before setting it.
Saving Tasks to localStorage
useEffect(() => {
if (isLoaded) {
localStorage.setItem("tasks", JSON.stringify(taskList))
}
}, [taskList, isLoaded])
Recall the lesson on useEffect and dependency arrays. We only want save tasks to localStorage when taskList
changes.
However, we also needed to add an isLoaded
state and dependency because when React initially renders, it will overwrite the taskList with an empty array.
Essentially what's happening is on mount, the taskList array is initialized as an empty array. The first useEffect gets the tasks in localStorage, and tries to set state with those tasks. However, state updates are asynchronous, and what happens is the second useEffect runs and overwrites the tasks with our initial taskList, which is an empty array.
So to address this, we "guard" our localStorage from being set only if isLoaded is true. We'll need to update our initial useEffect to update isLoaded:
useEffect(() => {
const storedTasks = localStorage.getItem("tasks")
if (storedTasks) {
try {
setTaskList(JSON.parse(storedTasks))
setIsLoaded(true)
} catch (error) {
console.error("Failed to parse tasks from localStorage", error)
}
}
}, [])
Filtering Tasks
Alright, now that we've persisted the tasks in localStorage, we want to implement a feature to allow users to filter which tasks they want to view. There's a few ways to tackle this problem, but the method I'll use is by creating a filter state to store which filter the user wants to use ("all", "completed", "active"):
const [filter, setFilter] = useState("all")
Filter Controls
Now let's create some buttons to set the filter state. To keep the code clean, I've decided to create a FilterButtons
component which takes the filter props:
const FilterButtons = ({ filter, setFilter }) => {
return (
<div className="flex gap-4 mt-8">
<button
type="button"
onClick={() => setFilter("all")}
className={`px-2 py-1 cursor-pointer rounded-md border-2 border-slate-500 hover:bg-gray-100 ${
filter === "all"
? "bg-slate-500 text-white"
: "bg-white text-slate-500"
}`}
>
All
</button>
<button
type="button"
onClick={() => setFilter("active")}
className={`px-2 py-1 cursor-pointer rounded-md border-2 border-slate-500 hover:bg-gray-100 ${
filter === "active"
? "bg-slate-500 text-white"
: "bg-white text-slate-500"
}`}
>
Active
</button>
<button
type="button"
onClick={() => setFilter("completed")}
className={`px-2 py-1 cursor-pointer rounded-md border-2 border-slate-500 ${
filter === "completed"
? "bg-slate-500 text-white"
: "bg-white text-slate-500 hover:bg-gray-100"
}`}
>
Completed
</button>
</div>
)
}
Conditionally Rendering Tasks
Now we need to conditionally render the tasks depending on what the filter status is. We can do that by chaining the .filter()
and .map()
methods together.
{taskList
.filter((task) => {
if (filter === "all") return true
if (filter === "completed") return task.isComplete
if (filter === "active") return !task.isComplete
})
.map((task) => (
<TaskItem
key={task.id}
task={task}
toggleComplete={toggleComplete}
deleteTask={deleteTask}
updateTask={updateTask}
/>
))}
So what's happening is we take our original taskList, and filter it. It loops through each task, and checks a condition. If that condition is true, then we keep that task in the array, if not then we remove it.
Conditionally Render Message for Empty List
Right now, if the task list is empty, it will only show an empty box, which doesn't look that great. So let's add a bit of logic to display a message if any of the filtered lists are empty. We could evaluate the length of the filtered tasks in line, however, it would be a bit cleaner to store the filtered tasks in a variable:
const filteredTasks = taskList.filter((task) => {
if (filter === "all") return true;
if (filter === "completed") return task.isComplete;
if (filter === "active") return !task.isComplete;
})
Then we'll dynamically update the message depending on what filter is selected:
let emptyMessage = "";
if (filteredTasks.length === 0) {
if (filter === "all") emptyMessage = "No tasks yet. Add some!";
else if (filter === "completed") emptyMessage = "No completed tasks yet.";
else if (filter === "active") emptyMessage = "No active tasks right now.";
}
Then for the render, we'll have to determine if the filtered list is empty or not and conditionally render the UI:
<div className="min-w-[400px] flex flex-col gap-4 mt-8 border border-gray-200 shadow p-8 rounded-md bg-white">
{filteredTasks.length > 0 ? (
filteredTasks.map((task) => (
<TaskItem
key={task.id}
task={task}
toggleComplete={toggleComplete}
deleteTask={deleteTask}
updateTask={updateTask}
/>
))
) : (
<p className="text-gray-400 text-center">{emptyMessage}</p>
)}
</div>
And there you have it! You've built a fully functional tasks tracking app. You've made phenomenal progress getting this far. However, the software development journey never ends. Once you get more comfortable with building single page applications in React, you'll want to start learning about fullstack development so that you can actually deploy full fledged applications for people to use.
When you're ready, check out some of our other courses to learn more!