Skip to content

ponqueli/ignite-timer

Repository files navigation

Logo

🔥 Ignite Timer 🔥 - Contador em React.

Resumo

  1. Visão geral do projeto
  2. Tecnologias utilizadas
  3. Instalação e utilização
  4. Conhecimentos aplicados

Visão geral do projeto

project preview project preview

Tecnologias utilizadas

Instalação e utilização

Pré-requisitos

Instalações necessárias

  1. NodeJS
  2. Yarn

Instalação

  1. Baixe as depedências do projeto com o comando $ yarn.
  2. Rode o projeto com o comando $ yarn dev. -> localhost:5173

Conhecimentos aplicados

Utilizando Layouts do React Router DOM

Essa funcionalidade permite a criação de layout para nossa aplicação. Por exemplo, imagine que temos um

que é exibido em diversas páginas do nosso App, cada vez que uma página é carregada o é carregado novamente. Utilizando os Layouts evitamos isso. Vamos conferir o código abaixo:

O componente Outled diz respeito ao local onde o resto da aplicação vai ser inserido, como se fosse o children...
// criando layout
import { Outlet } from 'react-router-dom'

import { Header } from '../components/Header'

export function DefaultLayout() {
  return (
    <>
      <Header />
      <Outlet />
    </>
  )
}
// configurando as rotas para usar o layout
import { Routes, Route, BrowserRouter } from 'react-router-dom'

import { DefaultLayout } from './layouts/DefaultLayout'
import { Home } from './pages/Home'
import { History } from './pages/History'

export function Router() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<DefaultLayout />}>
          <Route path="/" element={<Home />} />
          <Route path="/history" element={<History />} />
        </Route>
      </Routes>
    </BrowserRouter>
  )
}

Reaproveitando estilização de componentes no styled-components

const BaseInput = styled.input``

export const TaskInput = styled(BaseInput)``

Tipando elemento através da chave de outro elemento

const STATUS_COLOR = {
  green: 'green-500',
  yellow: 'yellow-500',
  red: 'red-500'
} as const //Indicando que os valores não são uma simples string

interface TaskStatusProps {
  statusColor: keyof typeof STATUS_COLOR
}

export const TaskStatus = styled.span<TaskStatusProps>`
  background: ${({ theme, statusColor }) => theme[STATUS_COLOR[statusColor]]};
`

Inferência de tipos de um formulário através do ZOD

const newCycleSchemaValidation = zod.object({
  task: zod.string().min(1)    
})

type NewCycleFormData = zod.infer<typeof newCycleSchemaValidation>

const { handleSubmit } = useForm<NewCycleFormData>({
  resolver: zodResolver(newCycleSchemaValidation),
  defaultValues: {
    task: '',
    minutesAmount: 0
  }
})

function handleCreateNewCycle(data: NewCycleFormData) {
  console.log(data)
}

Versionando LocalStorage

A prática de versionar o localStorage é usada para evitar bugs futuros na aplicação. Imagine que atualmente os dados são salvos de uma forma e, futuramente esse formato é alterado. Ao tentar ler os dados que já estavam no localStorage do usuário a aplicação vai bugar.

const localStorageKey = '@ignite-timer:cycles-state:v1.0'

Utilizando contextos do React

Utilizar contextos no React permite com que posssamos acessar valores de uma forma global entre todas as rotas da nossa aplicação.Para isso precisamos criar um provider no nível mais alto do nosso app, encapsulando todo o resto.

  <CyclesContextProvider>
    <Rotas />
  </CyclesContextProvider>

Agora dentro da rota desejada basta chamar o método useCyclesContext para ter acesso a todos os valores enviados pelo provider.

Utilizando Reducers no React

useReducers são utilizados para armazenar estado, como o hook useState. A principal diferenca entre os dois, é que os reducers tem a capacidade de armarzer uma estrutura de dados mais complexa de maneira mais fácil. O retorno da função useReducer é o estado e um método dispatch, usado para disparar uma action. O useReducer também espera receber três parâmetros. O primeiro deles é a própria função que vai tratar as actions recebidas do dispatch e alterar o estado; O segudo parâmetro são os valores inciais; Já o terceiro parâmetro é uma função responsável por carregar dados de fontes externas como o localStorage, sendo esse parâmetro, opcional.

 const [cyclesState, dispatch] = useReducer(
    cyclesReducer,
    {
      cycles: [],
      activeCycleID: null
    },
    //nunca pode retornar valor undefined
    loadDataFromLocalStorage
  )

Separando Actions e Reducers utilizando pattern

Para padronizar as nomenclaturas das Actions, pode ser criado um ENUM exportando-o para os arquivos que vão utiliza-lo.

export enum ActionTypes {
  ADD_NEW_CYCLE = 'ADD_NEW_CYCLE', 
}

Para padrozinar os argumentos enviados através do dispatch, podem ser criadas funções, que esperam esses argumentos de forma padronizada e, retornam uma action para o dispatch.

export function addNewCycleAction(newCycle: Cycle) {
  return {
    type: ActionTypes.ADD_NEW_CYCLE,
    payload: {
      newCycle
    }
  }
}

//utilizando método no dispatch
 function createNewCycle({ task, minutesAmount }: CreateCycleData) {
    const newCycle = {
      id: uuidv4(),
      task,
      minutesAmount,
      startDate: new Date()
    }

    dispatch(addNewCycleAction(newCycle))
  }

Para separar e padronizar as actions que manipulam o estado dentro do reducer, pode ser criado um arquivo separado seguindo o padão abaixo:

export function cyclesReducer(state: CycleStateReducer, action: any) {
  switch (action.type) {
    case ActionTypes.ADD_NEW_CYCLE:
      return produce(state, (draft) => {
        draft.cycles.push(action.payload.newCycle)
        draft.activeCycleID = action.payload.newCycle.id
      })
    default:
      return state
  }

Immer para manipulações complexas de estado

Obecendo o conceito de imutabilidade do React, algumas operações podem se tornar confusas, pensando nisso podemos instalar o Immer em nossa aplicação. O Immer cria um rascunho do estado onde podemos manipular os dados da forma convêncional, depois ele repassa esses dados do rascunho para o estado obedecendo as regras de imutabilidade.

//com immer
case ActionTypes.INTERRUPT_CURRENT_CYCLE: {
  const currentCycleIndex = state.cycles.findIndex(
    (cycle) => cycle.id === state.activeCycleID
  )

  if (currentCycleIndex < 0) return state

  return produce(state, (draft) => {
    draft.cycles[currentCycleIndex].interruptedDate = new Date()
    draft.activeCycleID = null
  })
}

//sem immer
 case 'INTERRUPT_CURRENT_CYCLE':
  return {
    cycles: state.cycles.map((currentCycle) => {
      if (currentCycle.id === state.activeCycleID) {
        return {
          ...currentCycle,
          interruptedDate: new Date()
        }
      } else {
        return currentCycle
      }
    }),
    activeCycleID: null
  }