DEV Community

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

Posted 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

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:


Criação do projeto

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

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

3 - (Opcional) Removendo arquivos desnecessários

commit: refactor: 🔥 remove vite unused files


Styled Components

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 - 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

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

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

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

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

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

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

Top comments (0)