To-do List

Create a to-do list with togglable and removable tasks.


Environment

Use the environment you are most comfortable with. I recommend using create-react-app to create a local version of the project so that you can inspect the DOM easily from your browser when debugging. Alternatively, you can work from a blank React template in CodeSandbox. Starter code is provided after the problem specification.

Specification

Write a functional component that accepts two props — an array of tasks and a function to update the array. Each element in the array is an object with two fields: “task” and “subtasks”. The “task” field is associated with a string description and the “subtasks” field is associated with an array of string descriptions. Here is an example of one such array (this array is also included in the starter code below):

[
  {
    task: "Clean bedroom",
    subtasks: ["Do laundry", "Organize desk", "Wipe floors"],
  },
  {
    task: "Study",
    subtasks: ["Review chemistry", "Do a React coding challenge"],
  },
  {
    task: "Build website",
    subtasks: ["Choose tech stack", "Design pages", "Develop", "Publish"],
  },
]

Your component should render a vertical list of the tasks along with the subtasks in an indented list below their associated task. The user should be able to toggle any subtask as completed/uncompleted by clicking on its text. Tasks do not need to be clickable. A completed subtask should appear as struck text. When all subtasks for a particular task are completed, the task should also appear struck through. Finally, include a button that removes all completed tasks and their associated subtasks when pressed (it should not remove completed subtasks associated with uncompleted tasks). The picture below shows the completed component after a user has clicked a few subtasks.

The completed component
The completed component

If the user were to click “Clear completed tasks” on the image above, only the “Study” task and its associated subtasks would be removed:

The component after clearing completed tasks
The component after clearing completed tasks

Starter Code

Here is some code to get you started:

import { useState } from "react";

const TASKS = [
  {
    task: "Clean bedroom",
    subtasks: ["Do laundry", "Organize desk", "Wipe floors"],
  },
  {
    task: "Study",
    subtasks: ["Review chemistry", "Do a React coding challenge"],
  },
  {
    task: "Build website",
    subtasks: ["Choose tech stack", "Design pages", "Develop", "Publish"],
  },
];

function App() {
  const [tasks, setTasks] = useState(TASKS);

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        marginTop: 30,
      }}
    >
      <TasksAndSubtasks taskArray={tasks} setTaskArray={setTasks} />
    </div>
  );
}

const TasksAndSubtasks = ({ taskArray, setTaskArray }) => {
  // YOUR CODE HERE
};

export default App;

The solution below only uses the TasksAndSubtasks component, but feel free to divide your code into more than one component.

Solution

import { useState } from "react";

const TASKS = [
  {
    task: "Clean bedroom",
    subtasks: ["Do laundry", "Organize desk", "Wipe floors"],
  },
  {
    task: "Study",
    subtasks: ["Review chemistry", "Do a React coding challenge"],
  },
  {
    task: "Build website",
    subtasks: ["Choose tech stack", "Design pages", "Develop", "Publish"],
  },
];

function App() {
  const [tasks, setTasks] = useState(TASKS);

  return (
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        marginTop: 30,
      }}
    >
      <TasksAndSubtasks taskArray={tasks} setTaskArray={setTasks} />
    </div>
  );
}

const TasksAndSubtasks = ({ taskArray, setTaskArray }) => {
  const [completed, setCompleted] = useState(() =>
    taskArray.map((taskObj) => taskObj.subtasks.map(() => false))
  );

  const flipCompleted = (outerIndex, innerIndex) =>
    setCompleted(
      completed.map((arr, index) =>
        index != outerIndex
          ? arr
          : arr.map((bool, jIndex) => (jIndex != innerIndex ? bool : !bool))
      )
    );

  const clearCompleted = () => {
    const completedCopy = [];

    setTaskArray(
      taskArray.filter((_, index) => {
        if (completed[index].some((value) => !value)) {
          completedCopy.push(completed[index]);
          return true;
        } else return false;
      })
    );
    setCompleted(completedCopy);
  };

  return (
    <div>
      <input
        type={"button"}
        onClick={clearCompleted}
        value={"Clear completed tasks"}
      />
      {taskArray.map((obj, i) => (
        <>
          <p>
            {completed[i].some((value) => !value) ? (
              obj.task
            ) : (
              <s>{obj.task}</s>
            )}
          </p>
          <div style={{ marginLeft: 20 }}>
            {obj.subtasks.map((subtask, j) => (
              <p onClick={() => flipCompleted(i, j)}>
                {completed[i][j] ? <s>{subtask}</s> : subtask}
              </p>
            ))}
          </div>
        </>
      ))}
    </div>
  );
};

export default App;

In the solution code above, we first create a state variable called completed that will store boolean values for each subtask. We pass a function to useState() that React will call to create the component’s initial state. The function returns an array of boolean arrays (ie a 2-dimensional array to mimic the structure of the task prop). For example, the last element of completed will be an array of 4 boolean values (all initialized to false). We then define two helper functions — one to flip a single boolean value in the completed array and another to clear completed tasks. These functions rely on Javascript’s filter() and map() functions (both of which return new arrays) to update the component’s state and prop arrays. The clearCompleted() function appends the boolean arrays associated with uncompleted tasks onto a new array while filtering completed tasks from the task array prop; this is necessary to ensure the structure of the completed array continues to mimic the structure of the task array.

Finally, we map over the task objects and subtask arrays to display the descriptions as described in the specification, passing the helper functions to the appropriate event handlers.

In this article, I do not assign keys to list item elements. Often, the unique ID that represents the item in the database from which it is fetched is used for the key attribute, but for simplicity, I do not include IDs in the task array. If keys are not assigned to HTML elements generated from an array, React will automatically use the indices of the array as keys. This can hurt performance and in some cases cause data to be displayed incorrectly. You can read more about keys and how React uses them to enhance performance here.


useState
filter