DEV Community

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

Posted on • Edited on

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

Neste artigo vou mostrar um dos projetos 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.

Já que uma das melhores maneiras de se aprender é ensinando, resolvi criar esta postagem para revisitar e fixar melhor os fundamentos da biblioteca React.

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

Ao longo do projeto vamos ver sobre vários assuntos:

  • Estilização com Styled Components;
  • Roteamento de páginas com React Router;
  • Distribuição de estado com Context API;
  • Efeito colateral do hook useEffect;
  • entre outros assuntos...

Nesta primeira parte iremos cobrir a estrutura da aplicação, a criação do projeto e a utilização da biblioteca Styled Components para estilização da nossa aplicação.

Links úteis:

Capítulos:


1 - Criação do projeto

1.1 - Usando o Vite

Vamos criar o projeto usando uma ferramenta chamada Vite que consegue fazer toda a configuração de um projeto frontend, configurando, por exemplo, o bundler e o compiler da nossa aplicação, nos poupando bastante tempo.

$ npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

Após executar o comando, dê um nome para a aplicação, após selecione React e TypeScript.

commit: feat: 🎉 add initial structure $ npm create vite@latest

1.2 - Instalando as dependências

Após o projeto ter sido criado, na raiz do projeto, instale as dependências com o comando:

$ npm i
Enter fullscreen mode Exit fullscreen mode

commit: build: 🔧 install dependencies $ npm i

1.3 - (Opcional) Removendo arquivos desnecessários

commit: refactor: 🔥 remove vite unused files


2 - Styled Components

2.1 - Instalando o pacote styled-components

Execute o comando na raiz do projeto:

$ npm i styled-components
Enter fullscreen mode Exit fullscreen mode

commit: build: ➕ add styled-components $ npm i styled-components

2.2 - Instalação da tipagem

O pacote styled-components era um daqueles pacotes que não traziam a tipagem do TypeScript junto dele:

styled-components versão 5 sem tipagem

desta forma, era preciso instalar a tipagem separadamente com esse comando:

# NÃO É MAIS PRECISO INSTALAR

$ npm i @types/styled-components -D
Enter fullscreen mode Exit fullscreen mode

-D é uma abreviatura de --save-dev que significa que será instalado apenas para desenvolvimento, ou seja, essa biblioteca não fará parte do build final da aplicação em produção. Isso porque o código TypeScript é convertido todo para JavaScript, então os tipos não serão usados no código final em produção.

Hoje em dia nas versões mais atuais, isso não é mais preciso, pois a tipagem já vem junto dele:

styled-components versão 6 com tipagem

Você pode ignorar os commits abaixo, pois um deles possue a instalação dos tipos e o outro a desinstalação. Isso ocorreu, pois as aulas do Rocketseat estavam desatualizadas no momento. Mas como eu instalei a versão mais recente, pude desinstalar sem problema o pacote dos tipos.

commit: build: ➕ add types of styled-components $ npm i @types/styled-components -D

commit: build: ➖ remove @types/styled-components with $ npm uninstall @types/styled-components

2.3 - Instale a extensão no VSCode

vscode-styled-components

Essa extensão habilita o Syntax highlighting, ou seja, deixa o código CSS colorido dentro do Tagged templates

2.4 - Entendendo como funciona o styled-components

Vamos criar um componente de botão para exemplificar o funcionamento do styled-components.

Crie o arquivo src/components/Button.tsxcom o código:

export function Button() {
 return <button>Enviar</button>
}
Enter fullscreen mode Exit fullscreen mode

Em src/App.tsx vamos repetir várias vezes o botão em tela:

import { Button } from "./components/Button";

export function App() {
 return (
  <>
   <Button />
   <Button />
   <Button />
   <Button />
  </>
 )
}
Enter fullscreen mode Exit fullscreen mode

Os sinais <> e </> chamado Fragment é usado quando precisamos colocar vários components dentro sem precisar de uma tag div

aparecendo 4 botões em tela sem estilização

Vamos agora adicionar uma propriedade para mudar a cor do botão.

Em src/components/Button.tsx adicionamos uma tipagem das cores aceitas para cada botão:

interface ButtonProps {
 variant?: 'primary' | 'secondary' | 'danger' | 'success';
}

export function Button(props: ButtonProps) {
 return <button>Enviar</button>
}
Enter fullscreen mode Exit fullscreen mode

Em src/App.tsx, para cada botão, vamos adicionar a propriedade de cor variant:

import { Button } from "./components/Button";

export function App() {
 return (
  <>
   <Button variant="primary" />
   <Button variant="secondary" />
   <Button variant="success" />
   <Button variant="danger" />
   <Button />
  </>
 )
}
Enter fullscreen mode Exit fullscreen mode

Quando estamos utilizando o CSS tradicional, para estilizar precisamos utilizar as classes. Para isso, vamos utilizar o css-modules apenas para este exemplo, pois, mais adiante, vamos utilizar apenas o styled-components.

Criando o arquivo de css-modules em src/components/Button.module.css com as classes:

.button {
 width: 100px;
 height: 40px;
}
Enter fullscreen mode Exit fullscreen mode

vamos importar os estilos e colocar o className em src/components/Button.tsx

import styles from "./Button.module.css";

interface ButtonProps {
 variant?: 'primary' | 'secondary' | 'danger' | 'success';
}

export function Button(props: ButtonProps) {
 return <button className={styles.button}>Enviar</button>
}
Enter fullscreen mode Exit fullscreen mode

e o resultado agora são botões com tamanhos maiores:

botões com estilização e tamanhos maiores

e com as cores no mesmo arquivo src/components/Button.tsx:

import styles from "./Button.module.css";

interface ButtonProps {
 variant?: 'primary' | 'secondary' | 'danger' | 'success';
}

export function Button({ variant = 'primary' }: ButtonProps) {
 return <button className={`${styles.button} ${styles[color]}`}>Enviar</button>
}
Enter fullscreen mode Exit fullscreen mode

Agora falta criar as classes para as cores. No arquivo de CSS src/components/Button.module.css:

.button {
 width: 100px;
 height: 40px;
}

.primary {
 background-color: purple;
}

.secondary {
 background-color: orange;
}

.danger {
 background-color: red;
}

.success {
 background-color: green;
}
Enter fullscreen mode Exit fullscreen mode

e o resultado das cores é esse:

botões com cores

Refatorando usando Styled Components

Vamos renomear o arquivo de CSS src/components/Button.module.css para ➡️ src/components/Button.styles.ts terminando exatamente com .ts e o conteúdo:

import styled from "styled-components";

export const ButtonContainer = styled.button`
  width: 100px;
  height: 40px;
`;
Enter fullscreen mode Exit fullscreen mode

e voltando no arquivo src/components/Button.tsx vamos trocar a tag button para ButtonContainer:

import { ButtonContainer } from "./Button.styles";

interface ButtonProps {
 variant?: 'primary' | 'secondary' | 'danger' | 'success';
}

export function Button({ variant = "primary" }: ButtonProps) {
  return <ButtonContainer>Enviar</ButtonContainer>;
}
Enter fullscreen mode Exit fullscreen mode

Como resultado temos os botões com os mesmos tamanhos, pois definimos o height e o width no ButtonContainer:

botões com os mesmos tamanhos usando styled components

Podemos definir a cor adicionando variant={variant} no mesmo arquivo src/components/Button.tsx:

import { ButtonContainer } from "./Button.styles";

interface ButtonProps {
 variant?: 'primary' | 'secondary' | 'danger' | 'success';
}

export function Button({ variant = "primary" }: ButtonProps) {
  return <ButtonContainer variant={variant}>Enviar</ButtonContainer>;
}
Enter fullscreen mode Exit fullscreen mode

e no arquivo de estilização src/components/Button.styles.ts:

import styled from "styled-components";

interface ButtonContainerProps {
  variant: "primary" | "secondary" | "danger" | "success";
}

export const ButtonContainer = styled.button`
  width: 100px;
  height: 40px;
`;
Enter fullscreen mode Exit fullscreen mode

nesse mesmo arquivo vamos criar um tipo para variant:

import styled from "styled-components";

export type ButtonVariant = "primary" | "secondary" | "danger" | "success";

interface ButtonContainerProps {
  variant: ButtonVariant;
}

export const ButtonContainer = styled.button`
  width: 100px;
  height: 40px;
`;
Enter fullscreen mode Exit fullscreen mode

E no arquivo src/components/Button.tsx vamos importar e usar essa tipagem:

import { ButtonContainer, ButtonVariant } from "./Button.styles";

interface ButtonProps {
  variant?: ButtonVariant;
}

export function Button({ variant = "primary" }: ButtonProps) {
  return <ButtonContainer variant={variant}>Enviar</ButtonContainer>;
}
Enter fullscreen mode Exit fullscreen mode

Para completar a tipagem e fazer uso dela, vamos adicionar <ButtonContainerProps>

import styled from "styled-components";

export type ButtonVariant = "primary" | "secondary" | "danger" | "success";

interface ButtonContainerProps {
  variant: ButtonVariant;
}

export const ButtonContainer = styled.button<ButtonContainerProps>`
  width: 100px;
  height: 40px;
`;
Enter fullscreen mode Exit fullscreen mode

Tipagem feita, agora precisamos setar as cores no mesmo arquivo de estilização src/components/Button.styles.ts adicionando a constante buttonVariants:

import styled from "styled-components";

export type ButtonVariant = "primary" | "secondary" | "danger" | "success";

interface ButtonContainerProps {
  variant: ButtonVariant;
}

const buttonVariants = {
  primary: "purple",
  secondary: "orange",
  danger: "red",
  success: "green",
};

export const ButtonContainer = styled.button<ButtonContainerProps>`
  width: 100px;
  height: 40px;
`;
Enter fullscreen mode Exit fullscreen mode

e fazendo uma interpolação de strings ${}, o Styled Components envia todas as propriedades automaticamente para a função:

import styled from "styled-components";

export type ButtonVariant = "primary" | "secondary" | "danger" | "success";

interface ButtonContainerProps {
  variant: ButtonVariant;
}

const buttonVariants = {
  primary: "purple",
  secondary: "orange",
  danger: "red",
  success: "green",
};

export const ButtonContainer = styled.button<ButtonContainerProps>`
  width: 100px;
  height: 40px;

  ${props => {
    return `background-color: ${buttonVariants[props.variant]}`
  }}
`;
Enter fullscreen mode Exit fullscreen mode

o props.variant é o valor que vem automaticamente do arquivo do componente do botão.

E o resultado com as cores permanece identicando:

botões com cores usando styled components

Conclusão do Styled Components: A vantagem é não precisar usar multiplas classes para a estilização e mantendo um scopo de componente.

Apenas uma melhoria na questão de Syntax highlighting:

sem css syntax highlighting

Para melhorar podemos importar o css e acrescentar a palavra css na frente da template literals:

com css syntax highlighting

commit: feat: ✨ add button component to understand about styled-components

2.5 - Configurando Temas

Crie o arquivo src/styles/themes/default.ts com o seguinte código:

export const defaultTheme = {
  white: "#FFF",
  primary: "#8257e6",
  secondary: "orange",
};
Enter fullscreen mode Exit fullscreen mode

No arquivo src/App.tsx vamos importar o defaultTheme e vamos passar como valor a propriedade theme do ThemeProvider:

import { ThemeProvider } from "styled-components";
import { Button } from "./components/Button";

import { defaultTheme } from "./styles/themes/default";

export function App() {
  return (
    <ThemeProvider theme={defaultTheme}>
      <Button variant="primary" />
      <Button variant="secondary" />
      <Button variant="success" />
      <Button variant="danger" />
      <Button />
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Se tivessemos mais de um tema, poderíamos trocar o valor defaultTheme para lightTheme por exemplo: theme={lightTheme}

Podemos acessar as cores do tema usando props.theme.nomeDaPropriedade:

`
  background-color: ${(props) => props.theme.primary};
`
Enter fullscreen mode Exit fullscreen mode

No arquivo do botão src/components/Button.styles.ts:

...

export const ButtonContainer = styled.button<ButtonContainerProps>`
  width: 100px;
  height: 40px;
  border-radius: 4px;
  border: 0;
  margin: 8px;
  background-color: ${(props) => props.theme.primary};
  color: ${(props) => props.theme.white};
`;
Enter fullscreen mode Exit fullscreen mode

e temos esse resultado:

botões com o tema aplicado

commit: feat: ✨ add themes

2.6 - Tipagem de Temas

Temos um problema de autocomplete ao digitar props.theme. o VSCode não nos fornece as opções disponíveis, isso acontece por falta de configuração de tipagem:

sem autocomplete ao digitar  raw `props.theme.` endraw

Para corrigir isso, vamos criar um arquivo no diretório src/@types/styled.d.ts.

pasta @types

A nomenclatura @types foi usada nesse caso simplismente para permanecer no topo da árvore de pastas. Além de ficar com o ícone do TypeScript.

Já a nomenclatura do arquivo styled.d.ts terminando com d.ts, indica que esse arquivo possuí apenas código de tipagem do TypeScript, chamado de arquivo de definição de tipos.

Nesse arquivo src/@types/styled.d.ts vamos adicionar duas importações:

import "styled-components";
import { defaultTheme } from "../styles/themes/default";
Enter fullscreen mode Exit fullscreen mode

ponteiro do mouse descansando em cima da const  raw `defaultTheme` endraw

A imagem acima mostra o ponteiro do mouse descansando em cima da const defaultTheme, indicando que a devolução do valor é do tipo objeto, com as propriedades do tipo string, ou seja, mostra a tipagem do defaultTheme. Isso acontece pois o TypeScript cria a tipagem de forma automática pra gente, mas existe uma forma de nós acessarmos isso e guardar em uma variável com o seguinte código:

type ThemeType = typeof defaultTheme;

// ThemeType poderia ter qualquer nome
Enter fullscreen mode Exit fullscreen mode

guardando o tipo em uma variável

Na imagem acima podemos perceber que o tipo gerado (inferência de tipo) para o defaultTheme foi guardado dentro de ThemeType, isso graças ao typeof que capturou a tipagem do defaultTheme.

No arquivo src/@types/styled.d.ts vamos fazer o uso do declare module:

import "styled-components";
import { defaultTheme } from "../styles/themes/default";

type ThemeType = typeof defaultTheme;

declare module "styled-components" {
  // Aqui vamos subscrever a tipagem do DefaultTheme
}
Enter fullscreen mode Exit fullscreen mode

O declare module cria uma tipagem para o módulo styled-components do NPM, e toda vez que o styled-components for importado, a tipagem, a definição de tipos que vai ser puxada, vai ser o que foi definido dentro do scopo do declare module.

Como queremos apenas subscrever algo de dentro do declare module, e não criar toda uma tipagem nova, nós tivemos que fazer a importação import "styled-components"; e fazer a declaração com o declare module "styled-components". Sem a importação estaríamos criando tudo do zero a definição de tipos do pacote styled-components.

theme sem tipagem

Na imagem acima, mostra o arquivo src/components/Button.styles.ts, e nele o ponteiro do mouse está parado em cima da palavra theme. Se clicar em cima de theme segurando a tecla ctrl ou command, é direcionado para o arquivo de tipos do styled-components:

arquivo de tipos do  raw `styled-components` endraw

Agora clicando em cima de DefaultTheme em laranja, é direcionado para a exportação da tipagem do DefaultTheme:

exportação da tipagem do  raw `DefaultTheme` endraw

Na imagem acima, podemos ver que o objeto {} do DefaultTheme exportado está vazio, pois esse objeto foi feito para ser subscrito pelas nossas tipagens.

Então o código final do arquivo src/@types/styled.d.ts ficará assim:

import "styled-components";
import { defaultTheme } from "../styles/themes/default";

type ThemeType = typeof defaultTheme;

// subescreve a tipagem do DefaultTheme
declare module "styled-components" {
  export interface DefaultTheme extends ThemeType {}
}
Enter fullscreen mode Exit fullscreen mode

Agora no arquivo src/components/Button.styles.ts podemos ver a tipagem funcionando:

tipagem funcionando para o tema

e se tentar colocar uma propriedade que não existe, um erro será acusado:

erro ao usar propriedade que não existe

commit: feat: ✨ add type to themes and override DefaultTheme

2.7 - Estilos Globais

Criando o arquivo no diretório src/styles/global.ts. Mais uma vez terminando com .ts pois estamos utilizando styled-components. Todo CSS global vamos colocar nesse arquivo:

import { createGlobalStyle } from "styled-components";

export const GlobalStyle = createGlobalStyle`
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  body {
    background: #333;
    color: #FFF;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Vamos agora importar no arquivo src/App.tsx e utilizar:

utilizando estilos globais do styled-components

import { ThemeProvider } from "styled-components";
import { Button } from "./components/Button";

import { GlobalStyle } from "./styles/global";
import { defaultTheme } from "./styles/themes/default";

export function App() {
  return (
    <ThemeProvider theme={defaultTheme}>
      <Button variant="primary" />
      <Button variant="secondary" />
      <Button variant="success" />
      <Button variant="danger" />
      <Button />

      {/* tanto faz a posição do <GlobalStyle /> dentro do ThemeProvider */}
      <GlobalStyle /> 
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tanto faz a posição do <GlobalStyle /> dentro do ThemeProvider, o importante é que <GlobalStyle /> esteja dentro do scopo do ThemeProvider, senão o <GlobalStyle /> não terá acesso as variáveis do nosso tema.

Acessando a aplicação, podemos ver os estilos globais sendo aplicados, como o background na cor cinza:

aplicação com fundo na cor cinza

commit: feat: ✨ add global styles

2.8 Cores e Fonte

Para este projeto foi escolhido a fonte Roboto e a fonte Roboto Mono

A fonte Roboto Mono é uma fonte tipográfica monoespaçada, cujas letras e caracteres ocupam o mesmo espaço horizontal. Ela será ideal para ser usada no timer.

Adicionamos as fontes no arquivo index.html:

<!doctype html>
<html lang="pt">

<head>
  <meta charset="UTF-8" />

  <!-- adicione essas duas linhas -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <!-- o ideal é deixar os dois links acima por primeiro para carregar mais rápido as fontes -->

  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  <!-- e adicione também esta linha -->
  <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,700;1,700&family=Roboto:wght@400;700&display=swap" rel="stylesheet" />

  <title>Ignite Timer</title>
</head>
Enter fullscreen mode Exit fullscreen mode

No arquivo de estilos globais src/styles/global.ts:

import { createGlobalStyle } from "styled-components";

export const GlobalStyle = createGlobalStyle`
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  body {
    background: #333;
    color: #FFF;
  }

  /* adicione */
  body, input, textarea, button {
    font-family: 'Roboto', sans-serif;
    font-weight: 400;
    font-size: 1rem;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Fonte tipográfica definida, agora vamos definir as cores no nosso tema, no arquivo src/styles/themes/default.ts:

export const defaultTheme = {
  white: "#FFF",

  "gray-100": "#E1E1E6",
  "gray-300": "#C4C4CC",
  "gray-400": "#8D8D99",
  "gray-500": "#7C7C8A",
  "gray-600": "#323238",
  "gray-700": "#29292E",
  "gray-800": "#202024",

  "gray-900": "#121214",
  "green-300": "#00B37E",
  "green-500": "#00875F",
  "green-700": "#015F43",

  "red-500": "#AB222E",
  "red-700": "#7A1921",

  "yellow-500": "#FBA94C",
};
Enter fullscreen mode Exit fullscreen mode

Vamos utilizar as cores no arquivo src/styles/global.ts:

import { createGlobalStyle } from "styled-components";

export const GlobalStyle = createGlobalStyle`
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  :focus {
    outline: none;
    box-shadow: 0 0 0 2px ${(props) => props.theme["green-500"]};
  }

  body {
    background: ${(props) => props.theme["gray-900"]};
    color: ${(props) => props.theme["gray-300"]};
  }

  body, input, textarea, button {
    font-family: 'Roboto', sans-serif;
    font-weight: 400;
    font-size: 1rem;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Ao digitar props.theme., por causa da tipagem, temos todas as cores disponiveis no autocomplete:

autocomplete das cores no arquivo global.ts

Pra finalizar essa parte de cores, precisamos arrumar o erro no arquivo de estilos do botão src/components/Button.styles.ts:

erro em props.theme.primary

Trocamos de props.theme.primary para props.theme["green-500"].

Feito isso já podemos fisualizar o novo estilo:

novo estilo

commit: feat: 💄 add fonts and colors

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs