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

Top comments (0)