- ReactJS
- React Hook Form
- React Router Dom
- Styled Components
- React-Toastify
- ViteJS
- Phospor React
- Yup
- Date-fns
- Zod
- Immer
- Vercel
Instalações necessárias
- NodeJS
- Yarn
- Baixe as depedências do projeto com o comando
$ yarn
. - Rode o projeto com o comando
$ yarn dev
. -> localhost:5173
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>
)
}
const BaseInput = styled.input``
export const TaskInput = styled(BaseInput)``
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]]};
`
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)
}
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'
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.
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
)
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
}
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
}