⚛️⏳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: Meu repositório do projeto Repositório oficial do Rocketseat Figma do projeto feito pela Rocketseat Capítulos: 1 - Iniciando novo ciclo 1.1 - Closures no React 1.2 - Apenas um ciclo ativo 2 - Criando countdown 2.1 - Separando minutos e segundos 2.2 - Instalando date-fns 2.3 - Reduzindo countdown com useEffect 2.4 - Usando return dentro do useEffect 4 - Timer no título do navegador 5 - Interrompendo o ciclo 1 - Iniciando novo ciclo Até agora o que nós temos é uma função chamada handleCreateNewCycle fazendo um console.log dos dados: 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([]); ... 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(); } no fim temos esse código adicionado: 1.1 - Closures no React Precisamos fazer uma pequena correção no código abaixo: setCycles([...cycles, newCycle]); Precisamos usar uma função para pegar sempre o estado atual. Isso é chamado de Closure: setCycles((state) => [...state, newCycle]); 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. 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 } 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(null); 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: E percorrer os ciclos para encontrar qual ciclo possui o ID do ciclo ativo: 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: 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 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; 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); 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 curr

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
- 1.1 - Closures no React
- 1.2 - Apenas um ciclo ativo
-
2 - Criando countdown
- 2.1 - Separando minutos e segundos
- 2.2 - Instalando date-fns
- 2.3 - Reduzindo countdown com
useEffect
- 2.4 - Usando
return
dentro douseEffect
- 4 - Timer no título do navegador
- 5 - Interrompendo o ciclo
1 - Iniciando novo ciclo
Até agora o que nós temos é uma função chamada handleCreateNewCycle
fazendo um console.log
dos dados:
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[]>([]);
...
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();
}
no fim temos esse código adicionado:
1.1 - Closures no React
Precisamos fazer uma pequena correção no código abaixo:
setCycles([...cycles, newCycle]);
Precisamos usar uma função para pegar sempre o estado atual. Isso é chamado de Closure:
setCycles((state) => [...state, newCycle]);
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
.
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
}
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);
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:
E percorrer os ciclos para encontrar qual ciclo possui o ID do ciclo ativo:
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
:
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
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;
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);
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;
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");
Entendendo esse cálculo:
Para transformar segundos em minutos, precisamos dividir por 60.
25 minutos * 60 = 1500 segundos
1500 segundos / 60 = 25 minutos
se tiver passado 1 segundo, ao invés de 1500 segundos, teremos 1499 segundos:
1499 segundos / 60 = 24,9833333333 minutos
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
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>
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
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
};
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