How I created my first React project as a beginner

ricardosxav

RicardoSXAV

Posted on May 14, 2021

How I created my first React project as a beginner

Table of Contents

Introduction

When you're a beginner, the most part of the time you spend learning new things and watching tutorials. Of course, that's a very important step in the process, but in order to really test your knowledge and see which aspects you need to improve, there's no better way than trying to do personal projects.

It doesn't need to be something big or fancy (mainly when you're a beginner), you just need to use what you've been learning. That's a good way to ensure that your study style is giving you results and that you're going in the right direction.

As a way to remember and document all the learnings from the project, also looking to help someone who is also trying to do their first project, I wrote this post explaining every step I took in details.

About the project

Click here to see the final result of the app.

I came up with this idea while using a Pomodoro website. In my mind, I would like to have something to count how much time I spent doing a task, so I can have an idea of how I'm spending my time over the day. That's the idea: a task chronometer. Yeah, nothing original or revolutionary. But it still a big challenge for me. Actually, there are plenty of functionalities on the project that I had no idea how to implement.

So, I thought it would be a good first project: something that can be useful, not too complicated, but with features that I would need to search and learn how to do.

Everything I used and every source that helped me I'll try to put here, to document the entire process. So, let's start!

Starting the project

To have a better idea of what I'll build, my first task was to make a wireframe of the application. I thought about use Figma, but it was too complex to me, and I would like to have something simpler. So I used Whimsical.

image

Prototyping

  • Click here to go to my design.

The website is basically divided into three sections: Chronometer Page, Task Page and Statistics Page. Let's see them in details.

Chronometer Page

image

It's the main page of the site, that I decided to call Tick Time. There's a simple chronometer, with a Navbar at the top and a button to add time to Selected Task. When you click it, you'll have a pop-up to confirm the time added.

Task Page

image

Where you can add or delete tasks, see the task list, select one task and see Completed Tasks. You can also click a task and see details about it:

  • Total time you spent in that task.
  • Sub-tasks that you can add and delete (nested task system 🤯).
  • An option to complete the task, sending it to Completed Task list.
  • Another option to delete the task.

Statistics Page

image

It shows the total time you spent doing tasks and it ranks every task by time.

Technologies

It will be mainly a front-end application. I'll use React, which is the library I'm current learning. Also, I have in mind that I'll need React Router and some other dependencies. As I move forward I'll comment on dependencies of the project.

Setting up initial code

I'll use Yarn as my package manager. That's the command to create a new react app folder using yarn.

Create React App

yarn create react-app tick-time
Enter fullscreen mode Exit fullscreen mode

React Router

The project will need React Router to make client-side routing. So I'll install it:

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

Font Awesome Icons

All the icons I'll use in the project are from Font Awesome. There are some ways to use font-awesome in your project. I just put this link in my index.html:

<link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
    />
Enter fullscreen mode Exit fullscreen mode

Google Fonts

I also imported Roboto fonts from Google Fonts to use.

GitHub

I created a repository in GitHub to save changes and keep the code.

Some commands

My knowledge with Git is very basic, but let me show you some commands:

git status
Enter fullscreen mode Exit fullscreen mode
  • This command will show all changes that you made
git add 
Enter fullscreen mode Exit fullscreen mode
  • I use this command to add all updates I did in code
git commit -m 'commit name'
Enter fullscreen mode Exit fullscreen mode
  • This command is to make a commit and give it a name (describe what you changed in code)
git push -u origin main
Enter fullscreen mode Exit fullscreen mode
  • I'm making the changes to my repository in a branch called main.

That's basically all the commands I'm using to put my project in GitHub.

Thinking about Components

Components are one of the cores of React, so it's very important to have a sense of which components you'll need to create your application. At least, that was something that I often saw during courses, and I believe it helps me to get a better picture of what I need to do.

So I created the diagram below with what I imagine is necessary to make the app:

image

Click here to see my diagram.
With that in mind, I'll start the application!

All components will be in /components, inside /src.

Chronometer - Component



I used this post as a reference to make the timer. It's very detailed and it uses React too.

Basically a timer is made up with two things: a stored value that adds 1 to it each second (when it's not paused) and a formatted way to show this value. If this value is 120 for example, we want to display 02:00. If it's 3600 (1 hour), we want to display 01:00:00.

That's our initial code:

import { useRef, useState } from "react";

function Timer() {
  const [time, setTime] = useState(0);
  const [isActive, setIsActive] = useState(false);
  const increment = useRef(null);

  function start() {
    increment.current = setInterval(() => setTime((time) => time + 1), 1000);
    setIsActive(true);
  }

  function pause() {
    clearInterval(increment.current);
    setIsActive(false);
  }

  function restart() {
    clearInterval(increment.current)
    setTime(0);
    setIsActive(false);
  }

  function addTime() {}

  return (
    <div>
      {time}
      {isActive ? (
        <button onClick={pause}>Pause</button>
      ) : (
        <button onClick={start}>Start</button>
      )}

      <button onClick={restart}>Restart</button>
      <button onClick={addTime}>Add Time</button>
    </div>
  );
}

export default Timer;
Enter fullscreen mode Exit fullscreen mode
  • I'm using useState hook to create a state for time (stored in seconds) and for a variable isActive (that will do the conditional rendering of start/pause buttons).
  • useRef give us an object that will persist until the end of component lifecycle. For more information, take a look here. We use setInterval to update the state with setTime each 1000ms (1 second).
  • Add time function is empty for now, because it will need to interact with other component (Tasks) to work.

Formatting

Now I need to format the value that's being rendered. That's our format function:

function formatTime(time) {
    const seconds = `${time % 60}`.padStart(2, "0");
    const minutes = `${Math.floor((time / 60) % 60)}`.padStart(2, "0");
    const hours = `${Math.floor(time / 3600)}`.padStart(2, "0");

    if (time >= 3600) {
      return `${hours} : ${minutes} : ${seconds}`;
    } else {
      return `${minutes} : ${seconds}`;
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • It takes a time as argument and convert it to a format mm:ss or hh:mm:ss depending on whether our time is greater or equal to 3600 seconds.

It's a function that can be used in any place, you just need to pass a time (in seconds) as an argument.

Chronometer - Styling

Style Folder

I created a folder inside /src called 'styles' to centralize everything related to styling.

Timer Popup

When you click to add time, there's a little window to confirm if you really want to do this. That's the way I did it:

<div
        className="Timer-popup"
        style={showPopup ? { display: "block" } : { display: "none" }}
      >
        <p>Time added to the TASK NAME</p>
        <button className="btn-popup-confirm" onClick={addTime}>
          OK
        </button>
        <button
          className="btn-popup-cancel"
          onClick={() => setShowPopup(false)}
        >
          Cancel
        </button>
      </div>
Enter fullscreen mode Exit fullscreen mode
  • I created a state showPopup with a initial value of false. When you click 'Add Time' button, showPopup is setted to true.
  • Style attribute is dinamically setting display according to showPopup value.
  • If you click Cancel, the popup is closed. If you click 'OK', then addTime() function is called.

I also applied a filter to the Timer. When popup is showing, opacity is setted to 0.5:

<div
        className="Timer"
        style={showPopup ? { filter: "opacity(0.5)" } : {}}
      >
Enter fullscreen mode Exit fullscreen mode

Navbar - Component

Using React Router

To create the Navbar, I had to put React Router inside the application.

After you install it, you just need put BrowserRouter between App (in index.js)

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

ReactDOM.render(<App />, document.getElementById("root"));
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);
Enter fullscreen mode Exit fullscreen mode

In App.js, I imported Route and Switch from react-router. That's how I configured the routes:

 <>
      <Navbar />
      <Switch>
        <Route exact path="/" render={() => <Timer />} />
        <Route exact path="/tasks" render={() => <Tasks />} />
        <Route exact path="/statistics" render={() => <Statistics />} />
      </Switch>
    </>
Enter fullscreen mode Exit fullscreen mode
  • exact is an attribute to ensure that our route will be exactly what we've putted in path.
  • Switch is to ensure that only one route will be showed (the first that match).

NavLink

Instead of using Link to make the navigation, I used NavLink, that works the same way, with a difference in stylization: you can pass it an attribute activeClassName, with a class that will be activated when you're on the link.

That way, I can style to have a different color and a border-bottom, like this:
image

import React from "react";
import { NavLink } from "react-router-dom";

import "../styles/navbar.css";
import Logo from "../assets/logo.png";

function Navbar() {
  return (
    <div className="Navbar">
      <div className="Navbar-logo">
        <img src={Logo} alt="Logo" />
      </div>
      <div className="links">
        <NavLink activeClassName="active-link" exact to="/">
          <i className="fas fa-clock" /> Chronometer
        </NavLink>
        <NavLink activeClassName="active-link" exact to="/tasks">
          <i className="fas fa-tasks" /> Tasks
        </NavLink>
        <NavLink activeClassName="active-link" exact to="/statistics">
          <i className="fas fa-signal" /> Statistics
        </NavLink>
      </div>
    </div>
  );
}

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

Navbar - Styling

Logo

To make the logo, I used Canva. I just put a timer icon besides the name.
image

  • That's the final result. Yeah, i's badly cutted, but as the application has a white background, there was not much problem.

Tasks - Components

Certainly the most challenging part of the whole application was doing the tasks functionality. But it was also something that helped me a lot to memorize all the information I learned.

I decided to create two components: a child stateless component rendering a single task (Task) and other containing all the state, rendering a list of tasks (TaskList).

Starting

Just remembering, that's how task data should look like:

[
{
      id: 1,
      taskName: "Study Javascript",
      totalTime: 3652,
      isCompleted: false,
      isSelected: true,
      subTasks: [{ subTaskName: "Learn about DOM", isCompleted: true }],
    },
    {
      id: 2,
      taskName: "Study CSS",
      totalTime: 2458,
      isCompleted: true,
      isSelected: false,
      subTasks: [{ subTaskName: "Study about flexbox", isCompleted: true }],
    },
]
Enter fullscreen mode Exit fullscreen mode

So, I used useState to store this value (later I'll use local storage to have persistance of data).

const [tasks, setTasks] = useState([])
Enter fullscreen mode Exit fullscreen mode

There's two different lists in the application: one for completed tasks, and other for tasks that need to be completed. So I created two functions to render them according to the value of isCompleted.

function renderTaskList() {
    const not_completed = tasks
      .filter((task) => task.isCompleted === false)
      .map((task) => (
        <Task
          key={task.id}
          id={task.id}
          name={task.taskName}
          isSelected={task.isSelected}
          isCompleted={task.isCompleted}
          toggleOne={toggleOne}
          remove={removeTask}
          renderWindow={renderWindow}
        />
      ));

    return not_completed;
  }

  function renderCompletedTasks() {
    const completed = tasks
      .filter((task) => task.isCompleted === true)
      .map((task) => (
        <Task
          key={task.id}
          id={task.id}
          name={task.taskName}
          isSelected={task.isSelected}
          isCompleted={task.isCompleted}
          toggleOne={toggleOne}
          remove={removeTask}
          renderWindow={renderWindow}
        />
      ));

    return completed;
  }

// In return()

<div className="Task-list">
        <h1>Task List</h1>
        <form onSubmit={submitTask}>
          <input
            className="task-input"
            type="text"
            placeholder="Add Task"
            value={inputTask}
            onChange={taskNameChange}
          />

          <button type="submit" className="submit-new">
            <i className="fas fa-plus-circle" />
          </button>
        </form>

        {renderTaskList()}

        <div className="divider" />

        <h1>Completed Tasks</h1>

        {renderCompletedTasks()}
      </div>
Enter fullscreen mode Exit fullscreen mode
  • .filter will return a array with tasks that match our condition. Then for each task in that array, I'll create a Task component passing down some props.

Functions

Functions are very good to perform actions and isolate the logics that belongs to an action. Sometimes they can seem like magic, you put a value in parenthesis, and then you have something back. Or they perform something in the application.

Let's start with a function to add task.

Adding Tasks

function addTask(name) {
    if (inputTask.length === 0) {
      setAlert("Please, enter a name");
    } else {
      setTasks([
        {
          id: uuidv4(),
          taskName: name,
          totalTime: 0,
          isSelected: false,
          isCompleted: false,
          subTasks: [],
        },
        ...tasks,
      ]);
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • It's a function that receives a name as an argument, and set the tasks state to be the atual state (using spread operator) and a new task object.
  • I'm using uuid as a dependency to generate unique ID's. That's important because there are some actions that we need to know about which task will be affected. We can use taskName, but name is not something unique. You can put whatever name you want in the task.
  • inputTask is a state that stores the name user puts in input.

Creating a alert popup

The alert popup that appears when you enter the form without filling input with a name is based in a state.

const [alert, setAlert] = useState("");

// In return()

<div
        className="alert-popup"
        style={alert ? { display: "block" } : { display: "none" }}
      >
        {alert}
        <br />
        <button onClick={() => setAlert("")}>OK</button>
      </div>
Enter fullscreen mode Exit fullscreen mode
  • When alert is blank, display is setted to none. You can use it to create personalized alerts. You just pass the name inside the state. If you want the alert to go away, just use setAlert("").

Removing Tasks

Anyway, let's keep talking about the functions.

function removeTask(id) {
    setShowWindow(false);
    setSelectedTask({});

    const filteredTasks = tasks.filter((task) => task.id !== id);
    setTasks(filteredTasks);
  }
Enter fullscreen mode Exit fullscreen mode
  • Ignore setShowWindow and setSelectedTask for now.
  • removeTask is a function that takes an ID as an argument, and then filter our task state selecting objects that have a different id. As I said, .filter() will return a new array.
  • That value is assigned to a variable, and then we set tasks to be that new array.

React Forms

Now, let's see where I'm using those functions and learn a little about React Forms.

To add task, everything start in the form:

<div className="Task-list">
        <h1>Task List</h1>
        <form onSubmit={submitTask}>
          <input
            className="task-input"
            type="text"
            placeholder="Add Task"
            value={inputTask}
            onChange={taskNameChange}
          />

          <button type="submit" className="submit-new">
            <i className="fas fa-plus-circle" />
          </button>
        </form>
Enter fullscreen mode Exit fullscreen mode
  • We set the value of the input to be inputTask (a value stored in state).
  • We have a event attribute 'onChange' that will call this function (taskNameChange) every time there's a change (we type something).

That's taskNameChange function:

function taskNameChange(event) {
    setInputTask(event.target.value);
  }
Enter fullscreen mode Exit fullscreen mode

Simple, isn't it? Our function will receive from onChange a event object. So we use event.target.value to set the value of inputTask state.

Okay, but when we call the function to add our task? On submit, that we put on

tag as an attribute onSubmit. And then I put submitTask function, that again takes an event:
function submitTask(event) {
    event.preventDefault();
    const taskName = inputTask;
    addTask(taskName);
    setInputTask("");
  }
  • event.preventDefault() will prevent our form to cause a reload in the page.
  • And here we use addTask with inputTask value. After we set it to be empty

Too many steps, there aren't a easier way of doing it? Actually, it is. I fount that post showing how to create a hook that makes all the logic of React forms for you, so you don't need to keep repeating all this process.

Passing Props Down

Now about the remove function, it's being called in Task component. Just remembering, I'm passing it as a prop when I'm rendering each task.

.map((task) => (
        <Task
          key={task.id}
          id={task.id}
          name={task.taskName}
          isSelected={task.isSelected}
          isCompleted={task.isCompleted}
          toggleOne={toggleOne}
          remove={removeTask}
          renderWindow={renderWindow}
        />

Inside Task component things are really short, that's the whole code inside Task.js:

import React from "react";
import "../styles/task.css";

function Task({
  id,
  name,
  isSelected,
  isCompleted,
  remove,
  toggleOne,
  renderWindow,
}) {
  return (
    <div
      className={`Task ${isSelected && "Task-active"} ${
        isCompleted && "Task-completed"
      }`}
    >
      <div
        className={`Task-text ${isSelected && "Task-text-active"}`}
        onClick={() => renderWindow(id)}
      >
        {name}
      </div>

      {isCompleted === false &&
        (isSelected ? (
          <i
            className="Task-toggle fas fa-toggle-on"
            onClick={() => toggleOne(id, isSelected)}
          />
        ) : (
          <i
            className="Task-toggle fas fa-toggle-off"
            onClick={() => toggleOne(id, isSelected)}
          />
        ))}
      <i className="fas fa-times-circle" onClick={() => remove(id)} />
    </div>
  );
}

export default Task;
  • I'm destructuring the props. Instead of doing 'Task(props)' and then have to write props.something everytime, I preferred to destructure it.
  • When you click the icon, the remove function is called with the id of this Task.
  • To select the task, I did a conditional rendering of a toggle-on icon (if isSelected is true) or a togge-off icon (if isSelected is false).

Selecting only one task

Now let me show you the toggleOne function:

function toggleOne(id, isSelected) {
    tasks.forEach((task) => {
      if (task.isSelected === true) {
        task.isSelected = false;
      }
    });

    const newObject = tasks.find((task) => task.id === id);
    newObject.isSelected = !isSelected;

    const filtered = tasks.filter((task) => task.id !== id);
    setTasks([newObject, ...filtered]);
  }
  • At the beginning, I'm setting all selected tasks to be isSelected = false.
  • .find will return the first object that matches the condition. In case of ID, there's only one.
  • Now I'm inverting isSelected of this object.
  • I'll make a new array without the old task version, and after that I set tasks to be that array with that new changed task (inverted value) at the beginning.

Task window

Each task when clicked should show a window containing the name of the task, the time you spent doing that task and show all subtasks related to this task.

In order to create this functionality, I made two states:

const [showWindow, setShowWindow] = useState(false);
const [selectedTask, setSelectedTask] = useState({});
  • The first one is a boolean that is true when the window should be rendered.
  • The second contains all the information necessary to display the window for a specific task.

That's the function responsible for render the window:

function renderWindow(id) {
    const selected = tasks.find((task) => task.id === id);
    setSelectedTask(selected);
    setShowWindow(true);
  }

And if you remember, inside Task.js that function is being called when you click on a div containing the task name. I'm setting selectedTask to be the id (that's a prop inside Task component). Then I set showWindow to be true.
If showWindow is true, that's rendered:

<div
        className={`Task-window ${
          selectedTask.isSelected && "window-selected"
        } ${selectedTask.isCompleted && "window-completed"}`}
        style={showWindow ? { display: "block" } : { display: "none" }}
      >
        <i
          className="fas fa-window-close"
          onClick={() => setShowWindow(false)}
        />
        <h1 className={`${selectedTask.isCompleted && "taskName-completed"}`}>
          {selectedTask.taskName}
        </h1>
        <p className="time-info">Total Time</p>
        <h3>{formatTime(selectedTask.totalTime)}</h3>
        <h4>List of Subtasks</h4>

        {selectedTask.isCompleted === false && (
          <form onSubmit={submitSubTask}>
            <input
              className="small-input"
              type="text"
              placeholder="Add Subtask"
              value={inputSubTask}
              onChange={subTaskNameChange}
            />

            <button type="submit" className="submit-new">
              <i className="fas fa-plus-circle" />
            </button>
          </form>
        )}

        <div
          className="subtasksList"
          style={selectedTask.subTasks?.length > 10 ? { overflow: "auto" } : {}}
        >
          {showWindow &&
            selectedTask.subTasks.map((subTask) => (
              <div key={subTask.id} className="single-subtask">
                {subTask.isCompleted ? (
                  <i
                    className="fas fa-check-square"
                    onClick={() => toggleCheck(subTask.id)}
                  />
                ) : (
                  <i
                    className="far fa-square"
                    onClick={() => toggleCheck(subTask.id)}
                  />
                )}
                <p
                  key={subTask.id}
                  className={`${
                    subTask.isCompleted ? "completed-task" : "uncompleted-task"
                  }`}
                >
                  {subTask.subTaskName}
                </p>
                <i
                  className="fas fa-times-circle"
                  onClick={() => removeSubTask(subTask.id)}
                />
              </div>
            ))}
        </div>
        {selectedTask.isCompleted ? (
          <button className="btn btn-undocomplete" onClick={undoComplete}>
            Undo Completed
          </button>
        ) : (
          <button className="btn btn-complete" onClick={completeTask}>
            Complete Task
          </button>
        )}

        <button
          className="btn btn-remove"
          onClick={() => removeTask(selectedTask.id)}
        >
          Delete Task
        </button>
      </div>
  • I'm applying a different style to the window according to isSelected and isCompleted value.
  • The usual conditional rendering using style attribute. Then we have a icon to close the window.
  • We have the information about the task. Here I use again the formatTime function to display selectedTask.totalTime. I could have a separate file exporting this function, but I just copied it from Timer component

Subtasks

Okay, let's take a look now in subtasks part. First of all, something that may be new for some people (it was for me too). That specific line.

style={selectedTask.subTasks?.length > 10 ? { overflow: "auto" } : {}}

That '?' after selectedTask.subTasks is something called Optional Chaining. Basically it's checking if there's an object selectedTask with a key of subTasks before run the length method. That's because at the beginning of application, selectedTask is a empty object, so that would trigger an error while you're running.

After that, there's a map creating a div for each subtask in selectedTask. Just like toggle, I have two icons from font-awesome and a function to toggle based on subTask.id.

  function toggleCheck(id) {
    const filtered = selectedTask.subTasks.filter(
      (subtask) => subtask.id !== id
    );

    const newObject = selectedTask.subTasks.find(
      (subtask) => subtask.id === id
    );
    newObject.isCompleted = !newObject.isCompleted;

    selectedTask.subTasks = [...filtered, newObject];

    const filteredTasks = tasks.filter((task) => task.id !== selectedTask.id);
    setTasks([selectedTask, ...filteredTasks]);
  }

A little big for a simple functionality like toggle, no? Yes, and now I'm seeing that too. Whatever, that's the logic:

  • I'm selecting with filter the subtasks with a different id.
  • A new object is created using find, which return the first (and the only, in that case) subtask with the id passed to the function.
  • isCompleted is inverted and then I set selectedTask.subTasks to be the filtered substasks + the new object with inverted isCompleted.
  • I filter the tasks looking for tasks that are not being updated (in that case, tasks that have an id different than the selectedTask).
  • Finally I set tasks to be selectedTask (that inside function with inverted value) and the filtered tasks.

We have also functions to add and to remove subtasks.

function addSubTask(name) {
    if (inputSubTask.length === 0) {
      setAlert("Please, enter a name");
    } else {
      selectedTask.subTasks.unshift({
        id: uuidv4(),
        subTaskName: name,
        isCompleted: false,
      });
    }
  }

function removeSubTask(id) {
    const filteredTasks = tasks.filter((task) => task.id !== selectedTask.id);
    const filteredSubTasks = selectedTask.subTasks.filter(
      (subtask) => subtask.id !== id
    );

    selectedTask.subTasks = filteredSubTasks;

    setTasks([selectedTask, ...filteredTasks]);
  }
  • Same setAlert from Timer, just copied the code. We're checking if our new input for subtasks have something typed inside.
  • Unshift is a function like .pop(), but instead of add something at the end of an array, it adds at the beginning.
  • Removing a subtask is basically filter not updated tasks, filter not removed subtasks, update selectedTask value and then set tasks to be updated selectedTask + not updated tasks.

This function to add subtask was working without local storage, but because it doesn't use setTasks, when I used local storage it was not working. That's the new version:

const filteredTasks = tasks.filter((task) => task.id !== selectedTask.id);
    selectedTask.subTasks.unshift({
      id: uuidv4(),
      subTaskName: name,
      isCompleted: false,
    });

    setTasks([selectedTask, ...filteredTasks]);
  • Now we're updating tasks state properly, setting tasks to be a new array.

Completing Tasks

To complete tasks, thing are simpler. There are two functions:

  function completeTask() {
    const filteredTasks = tasks.filter((task) => task.id !== selectedTask.id);
    selectedTask.isSelected = false;
    selectedTask.isCompleted = true;
    setTasks([selectedTask, ...filteredTasks]);
    setShowWindow(false);
  }

  function undoComplete() {
    const filteredTasks = tasks.filter((task) => task.id !== selectedTask.id);
    selectedTask.isCompleted = false;
    setTasks([selectedTask, ...filteredTasks]);
    setShowWindow(false);
  }

They're the same function, but completeTask() makes sure that we don't have a function that's selected and completed at the same time.

Connecting TaskList to Timer

We need to pass information from TaskList to Timer and vice versa, to have the selected task name showing in Timer and to have the time spent in selected task inside our task window.

First problem that you will face when trying to do something like this is that your data doesn't persist. When you refresh the site, you lose all data you made, states are setted to initial value.

To solve that problem, I knew that I would need local storage. The problem is: I thought it was easy to implement. After I tried by myself and failed miserably, I found that magical hook that can make all the job for you.

import { useEffect, useState } from "react";

function useStickyState(defaultValue, key) {
  const [value, setValue] = useState(() => {
    const stickyValue = window.localStorage.getItem(key);

    return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

export default useStickyState;

You just need to pass a default value, like you would do with a useState hook and a key (as a string). Now we have fixed values for time and tasks to work with.

const [time, setTime] = useStickyState(0, "time");
const [tasks, setTasks] = useStickyState([], "tasks");

Passing Props Up

To pass props from child to parent you need to create a function in the parent component and pass it as a prop to child. Inside the child component you'll call that function passing the information that's inside the component.

Selected Task

Let's see how I passed the information about the task name to Timer:

In App.js
// At the top
const [toggledTask, setToggledTask] = useStickyState("", "toggledTask");

function getTask(name) {
    setToggledTask(name);
  }

// In return()
<Route
          exact
          path="/"
          render={() => <Timer 
          //getTime={getTime} 
          taskName={toggledTask} />}
        />
<Route
          exact
          path="/tasks"
          render={() => (
            <TaskList
              // haveTimeToAdd={haveTimeToAdd}
              // setHaveTimeToAdd={setHaveTimeToAdd}
              // timeToAdd={timeToAdd}
              // setTimeToAdd={setTimeToAdd}
              toggleTask={getTask}
            />

When we call the function in TaskList, we'll change the state in App, that's passing its value to Timer as a prop.

In TaskList.js
const updateTimer = tasks.map((task) => {
      if (task.isSelected === true) {
        return task.taskName;
      }
    });

    toggleTask(updateTimer);

Now that's inside toggleOne function, calling toggleTask function (that's getTask in App.js, I just passed with a different name).

In Timer.js

When I change the state in App, this state is passed as prop to Timer (taskName). And after destructure it, I can use this:

<h2>{taskName}</h2>

Total Time

Now how I'm adding time to selected task:

In App.js
// At the top

  const [timeToAdd, setTimeToAdd] = useState(0);
  const [haveTimeToAdd, setHaveTimeToAdd] = useState(false);

  function getTime(time) {
    setHaveTimeToAdd(true);
    setTimeToAdd(time);
  }

// In return()

   <Route
          exact
          path="/"
          render={() => <Timer 
getTime={getTime} 
// taskName={toggledTask} />}
        />
        <Route
          exact
          path="/tasks"
          render={() => (
            <TaskList
              haveTimeToAdd={haveTimeToAdd}
              setHaveTimeToAdd={setHaveTimeToAdd}
              timeToAdd={timeToAdd}
              setTimeToAdd={setTimeToAdd}
              // toggleTask={getTask}
            />
          )}
        />

A function getTime that receives a time as argument, setting two states: one telling that there is something to add and other containing time (in seconds) to add.

In Timer.js
function addTime() {
    getTime(time);
    setShowPopup(false);
    setTime(0);
  }

Now our addTime function is working properly. We run getTime, setting haveTimeToAdd to true and setting timeToAdd to be the time (a state inside Timer).

In TaskList.js
useEffect(() => {
    if (haveTimeToAdd) {
      const filteredTasks = tasks.filter((task) => task.isSelected === false);
      const taskToAdd = tasks.find((task) => task.isSelected === true);
      taskToAdd.totalTime = taskToAdd.totalTime + timeToAdd;

      setTasks([taskToAdd, ...filteredTasks]);
      setHaveTimeToAdd(false);
      setTimeToAdd(0);
    }
  });
  • useEffect is a hook that runs every time our component render in screen. It's checking if there's time to add.
  • If haveTimeToAdd === true, then we'll filter tasks that are not selected, and then find selected task (there's only one).
  • Finally we add the time to the current time in selected task and setTasks to be a new array, haveTimeToAdd to false and timeToAdd to 0.

Statistics - Component

The last component in our application, very simple actually. It's a information containing the total time spent doing tasks and a list ranking tasks acording to the time.

Total Time

// In App component, at the top

const [totalTime, setTotalTime] = useStickyState(0, "totalTime");

function getTime(time) {
    setHaveTimeToAdd(true);
    setTimeToAdd(time);
    setTotalTime(totalTime + time);
  }

// In App component, in return()

  <Route
          exact
          path="/statistics"
          render={() => (
            <Statistics 
// sortTasks={sortTasks} 
totalTime={totalTime} />
          )}
        />

// In Statistics component, in return()

<div className="Statistics">
      <h1>General Statistics</h1>
      <div className="Statistics-totalTime">
        <i className="fas fa-hourglass-start" />
        <p>
          You have spent a total of {formatTime(totalTime)}{" "}
          {totalTime < 60
            ? "seconds"
            : totalTime > 60 && totalTime < 3600
            ? "minutes"
            : totalTime > 3600
            ? "hours"
            : ""}{" "}
          doing tasks!
        </p>
      </div>
  • We have a state in App.js storing the totalTime. When we add time to a task, we're adding it to totalTime too.
  • totalTime is being passed as a prop to Statistics component.
  • We are using it to display the time formatted. I also make a conditional rendering of the word after the time (seconds, minutes or hours).

Sorting Tasks

To sort tasks, I found a useful function in Javascript, which is called (guess what?) .sort(). Inside App I created that function:

function sortTasks() {
    const taskListString = localStorage.getItem("tasks");
    const taskList = JSON.parse(taskListString);

    if (taskList?.length > 0) {
      const sortedArray = taskList.sort((a, b) =>
        a.totalTime > b.totalTime ? -1 : 1
      );

      return sortedArray;
    }
  • From localStorage, I'll get the stored value of tasks. It will return a string value, so we need to parse it using JSON.pase().
  • If there is a taskList and the length of that taskList is greater than 0, you generate a sorted array.
  • In sort function we're comparing a specific value .totalTime from a single task. This will generate a new array, that will be returned from the function.

And I passed sortTasks function as a prop to Statistics. So now, I can have this:

// At the top of Statistics component
const sortedTasks = sortTasks();

// In return()

<h2>Sorting tasks by time</h2>
        {sortedTasks?.map((task, index) => {
          return (
            <div className="single-sorted-task">
              <div className="number-circle">{index + 1}</div>
              <p>{task.taskName}</p>
              <h3>{formatTime(task.totalTime)}</h3>

Additional Feature

Before finish the application, I decided to add another feature in Timer.
image
When you click that icon, I want to change between a stopwatch and a countdown.

Here's how I did it:

// New states

const [isStopwatch, setIsStopwatch] = useStickyState(true, "isStopwatch");

const [countDownStart, setCountDownStart] = useStickyState(
    false,
    "countDownStart"
  );

const [countSeconds, setCountSeconds] = useStickyState("", "countSeconds");
  const [countMinutes, setCountMinutes] = useStickyState("", "countMinutes");
  const [countHours, setCountHours] = useStickyState("", "countHours");
  const [countTime, setCountTime] = useStickyState(0, "countTime");
  const [originalCountTime, setOriginalCountTime] = useStickyState(
    0,
    "originalCountTime"
  );

// useRef and useEffect hooks

const decrement = useRef(countTime);

  useEffect(() => {
    if (countTime === 0 && countDownStart === true) {
      clearInterval(decrement.current);

      pause();
      setShowPopup(true);
    }
  });

// In return()

  <span className="circle" onClick={() => setIsStopwatch(!isStopwatch)}>
            <i
              className={
                isStopwatch ? `fas fa-stopwatch` : "fas fa-stopwatch-20"
              }
            />
          </span>

 {isStopwatch && countDownStart === false ? (
            formatTime(time)
          ) : isStopwatch === false && countDownStart === false ? (
            renderCountdow()
          ) : (
            <div>{formatTime(countTime)}</div>
          )}
  • isStopwatch is setting if is in stopwatch or in countdown mode.
  • countDownStart is looking if I started the countdown.
  • countSeconds, countMinutes and countHours are just for the form.
  • countTime is the actual time showed when countDownStart is true. originalCountTime is the time you submitted for the first time.
  • In useEffect I'm checking if the countdown is over. If it is, we pause it and time is automatically added.

That's our function to render the countdown:

function renderCountdow() {
    return (
      <form id="count-form" className="count-down-form">
        <input
          value={countHours}
          type="number"
          placeholder="00"
          min="0"
          max="24"
          onChange={handleHourChange}
        />
        :
        <input
          value={countMinutes}
          type="number"
          placeholder="00"
          min="0"
          max="59"
          onChange={handleMinuteChange}
        />
        :
        <input
          value={countSeconds}
          type="number"
          placeholder="00"
          min="0"
          max="59"
          onChange={handleSecondChange}
        />
      </form>
    );
  }

And here we have the changes I made in other functions:

function start() {
    if (toggledTask || toggledTask !== "") {
      if (isStopwatch) {
        increment.current = setInterval(
          () => setTime((time) => time + 1),
          1000
        );
        setIsActive(true);
      } else {
        const seconds = formatString(
          `${countHours.padStart(2, "0")}:${countMinutes.padStart(
            2,
            "0"
          )}:${countSeconds.padStart(2, "0")}`
        );

        if (countTime === 0) {
          setCountTime(seconds);
          setOriginalCountTime(seconds);
        }

        decrement.current = setInterval(
          () => setCountTime((time) => time - 1),
          1000
        );

        setIsActive(true);
        setCountDownStart(true);
      }
    // } else {
    // setAlert("Before start, select a task");
    }
  }
  • We're checking if toggledTask have a value or if it's empty.
  • If it's not stopwatch, we're formatting the string that's being passed in inputs to seconds, and then setting countTime and originalCountTime to be that value.

If you're curious about the function to format string I used, click here and check the second answer.

function pause() {
    // clearInterval(increment.current);
    clearInterval(decrement.current);
    // setIsActive(false);
  }

  function restart() {
    // clearInterval(increment.current);
    clearInterval(decrement.current);
    // setTime(0);
    setCountTime(0);
    setCountDownStart(false);
    // setIsActive(false);
  }

  function addTime() {
    if (isStopwatch) {
      getTime(time);
      setShowPopup(false);
      setTime(0);
    } else {
      getTime(originalCountTime);
      setShowPopup(false);
      setCountDownStart(false);
    }
  }
  • In pause, now we're clearing the interval of decrement.current too.
  • In restart same thing, clearing decrement.current interval, setting countTime to 0 and setting countDownStart to false.
  • In addTime, getTime is now using originalCountTime (when is not stopwatch) to pass it to Task component.

Mobile Responsiveness

In order to have something working also in mobile I decided to do some CSS work with media queries. I literally just used this:

@media screen and (max-width: 580px) {
/* Changes I want to apply when width is less than 580px */
}
  • I was seeing in developer tools what was strange at certain width, and then I applied different styles to make it looks reasonable in smaller screens.

Deployment

I used Firebase Hosting to deploy my application. I followed this tutorial and everything is working fine.

Conclusion

Making this project, as I was expecting, was not a very easy task. But, despite all the difficulties, it was a pleasant experience. Seeing the idea that you thought and designed finally working gives you a sense of accomplishment.

Most of the project I went through trial and error, mainly in CSS. It took a while, but it helped me better understand what works and what doesn't.

If you have something in mind and really want to do it, try to get started. You may not be feeling prepared, but in the end you may end up being surprised by what you have learned and in addition you can learn new things in the process.

After finishing, see what points you still need to improve and try to better direct your efforts to study these aspects.

💖 💪 🙅 🚩
ricardosxav
RicardoSXAV

Posted on May 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related