---
title: "Tạo ứng dụng pomodoro trong NextJS"
description: "Giới thiệu Pomodoro là một phương pháp học tập mà chúng ta sẽ tập trung học trong 25 phút sau đó sẽ có một khoảng nghỉ là 5 phút. Chúng ta sẽ tạo một app pomodo"
date: "2021-06-10"
author:
  name: "Nguyen Pham"
  role: "Xin chào. Mình thích lập trình và công nghệ. Đây là blog mình chia sẻ lại các kiến thức về lập trình, công nghệ."
  avatar: "http://0.gravatar.com/avatar/6fb61398e24046b34e13870dd6c357b9?s=128&d=mm&r=g"
category: "Blog"
tags: ["Blog"]
coverImage: "/blog/covers/tao-ung-dung-pomodoro-trong-nextjs-p1.png"
featured: false
readingTime: "6 min read"
---

## Giới thiệu

Pomodoro là một phương pháp học tập mà chúng ta sẽ tập trung học trong 25 phút sau đó sẽ có một khoảng nghỉ là 5 phút.

Chúng ta sẽ tạo một app pomodoro để thực hiện việc đếm giờ đó.

## Bắt đầu code

Chúng ta sẽ sử dụng NextJS và TailwindCSS. Để bắt đầu chúng ta có thể clone repo sau:

```
git clone https://github.com/nvni/next-tailwindcss.git pomodoro
cd pomodoro
yarn install
```

Để hiểu chi tiết cách cài đặt bạn có thể xem bài viết này: <https://2k1.org/su-dung-tailwind-css-trong-nextjs/1276/2021/>

Bắt đầu code với file `index.js`

```
import { useEffect, useState } from "react";
import Head from "next/head";

const timeType = {
  pomodoro: { time: 25 * 60, stopmessage: "Bạn đã hoàn thành pomodoro 🍅" },
  sbreak: {
    time: 5 * 60,
    stopmessage: "Bạn đã hết thời gian nghỉ bắt đầu làm việc thôi nào 😊",
  },
  lbreak: {
    time: 15 * 60,
    stopmessage: "Bạn đã hết thời gian nghỉ bắt đầu làm việc thôi nào 😉",
  },
};
function pomodoro() {
  const [time, setTime] = useState(25 * 60);
  const [active, setActive] = useState(false);
  const [inter, setInter] = useState();
  const [type, setType] = useState("pomodoro");
  const [currentTime, setCurrentTime] = useState();
  // Change type
  useEffect(() => {
    timeReset();
  }, [type]);

  // Send notification
  useEffect(async () => {
    console.log("time :>> ", time);
    if (time === 0) {
      if (Notification.permission == "granted") {
        navigator.serviceWorker.getRegistration().then(function (reg) {
          reg.showNotification(timeType[type].stopmessage);
        });
      }
      clearInterval(inter);
    }
  }, [time]);

  // Active time
  useEffect(() => {
    if (active) {
      setInter(
        setInterval(() => {
          setTime(
            Math.floor(
              timeType[type].time - (new Date().getTime() - currentTime) / 1000
            )
          );
          console.log(
            "((new Date().getTime()) - currentTime) :>> ",
            Math.floor(
              timeType[type].time - (new Date().getTime() - currentTime) / 1000
            )
          );
        }, 100)
      );
    }
    return () => {
      clearInterval(inter);
    };
  }, [active]);

  function timeStart() {
    setCurrentTime(new Date().getTime());
    if (Notification.permission != "granted") {
      alert("You need turn on Notification");
      Notification.requestPermission(function (status) {
        console.log("Notification permission status:", status);
      });
    }
    setActive((a) => !a);
  }

  function timeStop() {
    clearInterval(inter);
    setActive(false);
  }

  // resetTime
  function timeReset() {
    clearInterval(inter);
    setActive(false);
    setTime(timeType[type].time);
  }
  return (
    <div>
      <Head>
        <title>{type}</title>
        {type == "pomodoro" && (
          <link rel="icon" href="/1.svg" type="image/svg" sizes="16x16" />
        )}
        {type == "sbreak" && (
          <link rel="icon" href="/2.svg" type="image/svg" sizes="16x16" />
        )}
        {type == "lbreak" && (
          <link rel="icon" href="/3.svg" type="image/svg" sizes="16x16" />
        )}
      </Head>
      <div className="flex h-screen w-screen bg-gray-700  justify-center items-center">
        <div className="flex flex-col w-full min-w-md md:w-1/2 h-4/5 bg-white rounded-lg">
          <div className="flex flex-row w-full justify-between p-5 space-x-2">
            <div
              onClick={() => {
                setType("pomodoro");
              }}
              className={`flex-1 text-center text-xl border rounded-lg hover:bg-red-300 p-1 ${
                type == "pomodoro" && "bg-red-300"
              }`}
            >
              Pomodoro
            </div>
            <div
              onClick={() => {
                setType("sbreak");
              }}
              className={`flex-1 text-center text-xl border rounded-lg hover:bg-green-300 p-1 ${
                type == "sbreak" && "bg-green-300"
              }`}
            >
              Nghỉ
            </div>
            <div
              onClick={() => {
                setType("lbreak");
              }}
              className={`flex-1  text-center text-xl border rounded-lg hover:bg-blue-300 p-1  ${
                type == "lbreak" && "bg-blue-300"
              }`}
            >
              Nghỉ dài
            </div>
          </div>
          <div className="w-full text-9xl text-center">
            {`${parseInt(time / 60)
              .toString()
              .padStart(2, "0")} : ${(time % 60).toString().padStart(2, "0")}`}
          </div>
          {/* Control button */}
          <div className="w-full text-9xl text-center space-x-4">
            <button
              className="text-5xl p-5 rounded-lg border border-blue-400 hover:bg-blue-400 hover:text-white focus:outline-none"
              style={active ? { display: "none" } : { display: "initial" }}
              onClick={timeStart}
            >
              Start
            </button>
            <button
              className="text-5xl p-5 rounded-lg border border-yellow-400 hover:bg-yellow-400 hover:text-white focus:outline-none"
              style={active ? { display: "initial" } : { display: "none" }}
              onClick={timeStop}
            >
              Stop
            </button>

            <button
              className="text-5xl p-5 rounded-lg border border-red-400 hover:bg-red-400 hover:text-white focus:outline-none"
              onClick={timeReset}
            >
              Reset
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

export default pomodoro;
```

## Một vài vấn đề cần lưu ý:

Các hàm `setState` là không đồng bộ   
tham khảo <https://medium.com/@baphemot/understanding-reactjs-setstate-a4640451865b>

Muốn cập nhật dữ liệu state từ state cũ ta cần làm `setState(prevState => prevState+1)` Điều này là bắt buộc.

![](https://2k1.org/wp-content/uploads/2021/06/image.png)

do vậy khi ta muốn một state thay đổi và lấy gia trị của state thì cần cho vào hàm `useEffect(()=>{} , [state])`

Hiểu về cleanup effect:

Tài liệu: <https://vi.reactjs.org/docs/hooks-effect.html>

```
  useEffect(() => {
    // effect
    return () => {
      // clean
    };
  }, []);
```

Tại sao phải clean:

- Khi ta áp dụng một effect thì effect nó sẽ tồn tại. như chương trình trên thì ta có `setInterval` Nếu không clean thì khi gọi effect lần thứ 2 nó vẫn áp dụng tiếp => nó áp dụng 2 lần `setInterval` do vậy sau mỗi lần ta cân clean nó đi.
- Cách chạy nó như sau:
  - lần 1: effect (Khi component mount)
  - lần 2: clean effect -> effect
  - lần 3: clean effect -> effect
  - …..

Một số vấn đề với `setInterval`

```
setInterval(()=>{
// Code
},1000)
```

setInterval sẽ thực hiện code trong 1s nhưng khi đếm thời gian thì nó bị ảnh hưởng bởi một số hàm khác nên nó sẽ bị đếm thời gian sai: do vậy ở code trên ta sẽ thực hiện update theo thời gian lấy từ hàm `Date()`  khoảng thời gian update là 100ms

tham khảo: <https://stackoverflow.com/questions/6685396/execute-the-setinterval-function-without-delay-the-first-time>

Hiển thị notification

<https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications>

Sử dụng PWA

<https://github.com/shadowwalker/next-pwa>

- Lưu ý file `manifest,json` cần đầy thủ thông tin thì mới có thể dùng PWA được

Tham khảo thêm:

- <https://ozmoroz.com/2018/11/why-my-setstate-doesnt-work/>
- <https://viblo.asia/p/nhung-dieu-can-luu-y-va-su-dung-hook-trong-react-phan-2-gDVK2Ln0lLj>
