DEV Community

Cover image for ⚛️⏳Parte 4: Criando um Timer com Histórico em React
Dev Maiqui 🇧🇷
Dev Maiqui 🇧🇷

Posted on

⚛️⏳Parte 4: Criando um Timer com Histórico em React

Esta é a quarta parte do projeto que construí na formação React da Rocketseat, um projeto de duas páginas/telas, onde uma tela contém o timer, e a outra tela contém o histórico dos ciclos realizados.

Nesta quarta parte do projeto vamos focar nas funcionalidades da aplicação.

Caso queira adquirir os cursos da Rocketseat com o meu cupom de desconto Acesse esse link

Links úteis:

Capítulos:


1 - Iniciando novo ciclo

Até agora o que nós temos é uma função chamada handleCreateNewCycle fazendo um console.log dos dados:

handleCreateNewCycle com apenas  raw `console.log(data)` endraw  e  raw `reset()` endraw

Mas, na verdade, queremos que essa função inicie um novo ciclo, queremos que a aplicação tenha um ciclo ativo.

E, para refletir isso na interface, que um novo ciclo iniciou, precisamos ter um estado, que é a única forma de armazenar alguma informação no componente que vá fazer com que a interface reaja a essa informação, ou seja, que a interface mude.

Como vamos ter uma página de histórico dos ciclos, precisamos de uma lista, e precisamos que cada ciclo tenha um identificador.

Então, vamos criar um estado e uma interface no arquivo src/pages/Home/index.tsx:

import { useState } from "react";

interface Cycle {
  id: string;
  task: string;
  minutesAmount: number;
}

export function Home() {
  const [cycles, setCycles] = useState<Cycle[]>([]);
...
Enter fullscreen mode Exit fullscreen mode

e vamos setar o novo ciclo:

function handleCreateNewCycle(data: NewCycleFormData) {
  const newCycle: Cycle = {
    id: String(new Date().getTime()), // retorna em milissegundos
    task: data.task,
    minutesAmount: data.minutesAmount,
  };

  // [...cycles, newCycle] está criando uma nova lista, seguindo a recomendação de imutabilidade
  setCycles([...cycles, newCycle]);

  reset();
}
Enter fullscreen mode Exit fullscreen mode

no fim temos esse código adicionado:

código com estado, interface e setCycles sendo usado

1.1 - Closures no React

Precisamos fazer uma pequena correção no código abaixo:

setCycles([...cycles, newCycle]);
Enter fullscreen mode Exit fullscreen mode

Precisamos usar uma função para pegar sempre o estado atual. Isso é chamado de Closure:

setCycles((state) => [...state, newCycle]);
Enter fullscreen mode Exit fullscreen mode

Agora o state sempre trará a lista de ciclos mais recente antes da gente incluir o novo ciclo. Após a alteração, o state atual terá o nosso novo ciclo.

1.2 - Apenas um ciclo ativo

Temos a página com a lista de ciclos, mas, apenas um ciclo estará ativo, apenas um ciclo terá a condição Em andamento.

Mostrando a lista de ciclos com diferentes status, mas apenas um com status  raw `em andamento` endraw

E podemos pensar em duas formas de fazer isso. Uma delas seria colocando isActive no Cycle:

interface Cycle {
  id: string;
  task: string;
  minutesAmount: number;
  isActive: boolean
}
Enter fullscreen mode Exit fullscreen mode

Mas qual o problema de fazer assim?

O problema é quando eu setar um novo ciclo como ativo, eu vou ter que percorrer todos os ciclos e verificar qual estava ativo antes para setar como inativo. Então toda vez que a gente for setar um ciclo como ativo, vamos ter que fazer duas operações.

Existe uma alternativa melhor, que é manter um estado com o ID do ciclo ativo:

const [activeCycleId, setActiveCycleId] = useState<string | null>(null);
Enter fullscreen mode Exit fullscreen mode

Quando a aplicação inicializa, não terá nenhum ciclo ativo, então o activeCycleId começa com null.

Precisamos agora setar o activeCycleId quando um novo ciclo for criado:

adicionando  raw `setActiveCycleId(id)` endraw

E percorrer os ciclos para encontrar qual ciclo possui o ID do ciclo ativo:

adicionando  raw `cycles.find()` endraw

Quando abrir a aplicação pela primeira vez, o console.log(activeCycle) mostrará undefined, pois não há um ciclo ativo.

Após iniciar um novo ciclo clicando no botão ▶️ Começar, podemos ver os dados do ciclo ativo no console:

ciclo ativo no console

OBS: Toda vez que colocamos um console.log no React, acaba que aparece duas vezes o mesmo resultado no console. Isso acontece apenas em desenvolvimento e tem relação com o <React.StrictMode> no arquivo main.tsx. Isso acontece para o React conseguir detectar alguns tipos de bugs.

commit: feat: ✨ add newCycle and setActiveCycleId


2 - Criando countdown

Nesta parte, quando o ciclo ativo iniciar vamos fazer com que o timer vá reduzindo.

No arquivo src/pages/Home/index.tsx dentro da função Home.

O usuário informa em minutos, mas o timer irá reduzir em segundos:

const totalSeconds = activeCycle 
  ? activeCycle.minutesAmount * 60 
  : 0;
Enter fullscreen mode Exit fullscreen mode

Para transformar minutos em segundos, precisamos multiplicar por 60.

Como o timer irá reduzir a cada um segundo, precisamos de um estado que irá guardar a quantidade de segundos que já se passaram desde que o ciclo ativo iniciou:

const [amountSecondsPassed, setAmountSecondsPassed] = useState(0);
Enter fullscreen mode Exit fullscreen mode

Assim podemos calcular totalSeconds menos amountSecondsPassed para mostrar o tempo que já passou na tela. Existem várias estratégias, mas vamos usar essa.

const currentSeconds = activeCycle
  ? totalSeconds - amountSecondsPassed
  : 0;
Enter fullscreen mode Exit fullscreen mode

2.1 - Separando minutos e segundos

Precisamos calcular e separar currentSeconds em minutos e segundos para mostrar em tela.

const minutesAmount = String(
  Math.floor(currentSeconds / 60)
).padStart(2, "0");

const secondsAmount = String(
  currentSeconds % 60
).padStart(2, "0");
Enter fullscreen mode Exit fullscreen mode

Entendendo esse cálculo:

Para transformar segundos em minutos, precisamos dividir por 60.

25 minutos * 60 = 1500 segundos
Enter fullscreen mode Exit fullscreen mode
1500 segundos / 60 = 25 minutos
Enter fullscreen mode Exit fullscreen mode

se tiver passado 1 segundo, ao invés de 1500 segundos, teremos 1499 segundos:

1499 segundos / 60 = 24,9833333333 minutos
Enter fullscreen mode Exit fullscreen mode

24,9833333333 por ser um número quebrado não podemos mostrar assim em tela.

Quantos minutos cheios nós temos em 1499 segundos tirando os quebrados????

Temos 24 minutos! O outro minuto (0,9833333333) falta 1 segundo.

Então podemos sempre arredondar essa divisão para baixo usando Math.floor.

E para pegar os segundos, usamos o operador de resto (Operador Mod) % que pega o resto da divisão:

1499 segundos % 60 = 59 segundos
Enter fullscreen mode Exit fullscreen mode

E esse padStart serve para quê???

O método padStart() preenche a string com outra string (várias vezes, se necessário) até que a string resultante atinja o comprimento fornecido. O preenchimento é aplicado do início dessa string.

Quando um número for abaixo de 10, exemplo 9, quebraria o layout, mas com padStart garantimos que o 9 se torne 09.

Só falta agora mostrar em tela:

<CountdownContainer>
  <span>{minutesAmount[0]}</span>
  <span>{minutesAmount[1]}</span>
  <Separator>:</Separator>
  <span>{secondsAmount[0]}</span>
  <span>{secondsAmount[1]}</span>
</CountdownContainer>
Enter fullscreen mode Exit fullscreen mode

commit: feat: ✨ add countdown

2.2 - Instalando date-fns

Daqui a pouco precisaremos usar a função differenceInSeconds do pacote date-fns, por isso já vamos fazer a instalação dele:

$ npm i date-fns
Enter fullscreen mode Exit fullscreen mode

commit: chore: ➕ add date lib $ npm i date-fns

2.3 - Reduzindo countdown com useEffect

Existe o método setInterval que possibilita chamar uma função a cada um segundo, ou o tempo que você desejar. Mas esse tempo no navegador não é preciso, é apenas uma estimativa, ou seja, se você setar o tempo de 1 segundo, não quer dizer que sempre será 1 segundo exato. Essa estimativa de tempo leva em consideração desempenho da máquina, aba do navegador em segundo plano, entre outras coisas.

Então não podemos nos basear no tempo do setInterval para reduzir o contador, pois pode ser que o timer não fique correto!

Por isso, vamos gravar o exato momento em que o ciclo ativo iniciou, adicionando startDate ao Cycle:

const newCycle: Cycle = {
  id: String(new Date().getTime()),
  task: data.task,
  minutesAmount: data.minutesAmount,
  startDate: new Date(), // Date() grava data e horário
};
Enter fullscreen mode Exit fullscreen mode

Usando useEffect

Para reduzir o countdown vamos utilizar o setInterval dentro de um hook chamado useEffect. Se você não tem conhecimento sobre esse hook, deixarei uma sugestão de vídeo explicando sobre: Aprenda useEffect de uma vez por todas 🔥

Como visto no texto mais acima, existe um jeito onde o timer não ficará 100% certo, vamos ver a forma errada de usar o setInterval nesse caso:

forma errada de usar o setInterval

Pra nos ajudar a fazer da forma correta, vamos importar differenceInSeconds do pacote date-fns:

import { differenceInSeconds } from "date-fns";
Enter fullscreen mode Exit fullscreen mode

differenceInSeconds calcula a diferença de duas datas em segundos.

useEffect(() => {
  if (activeCycle) {
    setInterval(() => {
      setAmountSecondsPassed(
        differenceInSeconds(new Date(), activeCycle.startDate)
      );
    }, 1000);
  }
}, [activeCycle]);
Enter fullscreen mode Exit fullscreen mode

activeCycle entre colchetes ([activeCycle]) significa que toda vez que activeCycle mudar, o código dentro do useEffect será acionado.

O countdown está reduzindo, mas ainda existem bugs que vamos precisar corrigir ao longo do desenvolvimento.

commit: feat: ✨ add countdown

2.4 - Usando return dentro do useEffect

Existe um bug no nosso contador. Podemos perceber que após um ciclo de 10 minutos ter iniciado, ao tentar iniciar um novo ciclo de 20 minutos, por exemplo, o timer não começará nos 20 minutos.

Etapas para solucionar o Bug:

  • O useEffect será acionado toda vez que um novo ciclo iniciar, ou seja, quando a variável activeCycle mudar.
  • O que está acontecendo é que quando o useEffect é chamado, um novo setInterval está sendo criado, ou seja, não estamos usando sempre o mesmo setInterval.
  • Precisamos então reiniciar/resetar o setInterval quando um novo ciclo iniciar, ou seja, quando o useEffect for chamado novamente.
  • Para resetar precisamos usar a função clearInterval()
  • E para usar a função clearInterval no momento certo, ou seja, quando o useEffect for chamado novamente, precisamos usar o return de dentro do useEffect, mas também de dentro da condição if.
useEffect(() => {
  let interval: number;

  if (activeCycle) {
    interval = setInterval(() => {
      setAmountSecondsPassed(
        differenceInSeconds(new Date(), activeCycle.startDate)
      );
    }, 1000);
  }

  return () => {
    clearInterval(interval);
  };
}, [activeCycle]);
Enter fullscreen mode Exit fullscreen mode

Resumindo, o return dentro do useEffect não será executado na primeira vez que o useEffect for chamado, o return será executado nas próximas vezes que o useEffect for chamado.

E para finalizar a correção do bug, precisamos resetar o valor amountSecondsPassed, usando setAmountSecondsPassed(0); dentro da função handleCreateNewCycle:

  function handleCreateNewCycle(data: NewCycleFormData) {
    const newCycle: Cycle = {
      id: String(new Date().getTime()),
      task: data.task,
      minutesAmount: data.minutesAmount,
      startDate: new Date(),
    };

    setCycles((state) => [...state, newCycle]);
    setActiveCycleId(newCycle.id);
    setAmountSecondsPassed(0); // Adicione essa linha

    reset();
  }
Enter fullscreen mode Exit fullscreen mode

4 - Timer no título do navegador

Vamos agora alterar o title da página para mostrar o timer, ou seja, mostrar o timer na aba do navegador.

  useEffect(() => {
    if (activeCycle) {
      document.title = `${minutes}:${seconds}`
    }
  }, [minutes, seconds, activeCycle])
Enter fullscreen mode Exit fullscreen mode

código pra mostrar o timer na aba do navegador

resultado do timer na aba do navegador

OBS: Se não estiver na mesma aba, o timer no título não vai aparecer de 1 em 1 segundo certinho, pela questão que já foi comentada anteriormente sobre o setInterval.

commit: feat: ✨ add clearInterval and add timer to document.title


5 - Interrompendo o ciclo

Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

👋 Kindness is contagious

Please show some love ❤️ or share a kind word in the comments if you found this useful!

Got it!