Skip to content

Tutorial React Native & TypeScript

victor-kauan-coder edited this page Sep 28, 2026 · 2 revisions

Tutorial: React + TypeScript no TermoLOG

Tutorial em vídeo: https://drive.google.com/file/d/1vkMUWIOGPhheTyFeDZSaaDOoEnunI8ub/view?usp=sharing

Guia básico para qualquer integrante da equipe começar a desenvolver o dashboard web do TermoLOG. Cobre instalação, conceitos essenciais, comunicação com a API FastAPI, persistência (local e no servidor), navegação e gráficos.

Para facilitar, os exemplos usam um site genérico de lista de tarefas. No TermoLOG o padrão é o mesmo, só mudam os dados: onde estiver tarefa, pense em sala ou leitura de temperatura.


1. Por que React + TypeScript no TermoLOG

[ESP32 + DHT22] --BLE--> [Nó central] --HTTPS--> [FastAPI] --> [PostgreSQL]
                                                     ^
                                         [Dashboard web: React + TS]

O dashboard é o site onde o usuário acompanha as salas monitoradas, o histórico de temperatura/umidade, o status de alarme e os relatórios de qualidade. Ele faz requisições HTTP para a API, recebe JSON e mostra na tela.

Escolha Motivo
React Componentes reutilizáveis (ex.: um cartão de sala usado para todas as salas) e tela que se atualiza sozinha quando os dados mudam
TypeScript Tipos descrevem os dados do mesmo jeito que o Pydantic no FastAPI: se front e back não baterem, o erro aparece no editor
Vite Ambiente de desenvolvimento rápido, com recarregamento instantâneo
Ecossistema Bibliotecas prontas para rotas (React Router) e gráficos (Recharts)

2. Preparação do ambiente

2.1 Pré-requisitos

  • Node.js 22 LTS ou mais novo (download)
  • Git, VS Code e um navegador atualizado
  • Python 3.11 (só para a API de teste da Seção 5)

No Linux, prefira instalar o Node via nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# feche e abra o terminal
nvm install 22
nvm use 22

Confira a instalação com node -v e npm -v.

Extensões recomendadas: ES7+ React Snippets, ESLint e Prettier (VS Code) e React Developer Tools (navegador).

2.2 Criando o projeto

npm create vite@latest meu-site -- --template react-ts
cd meu-site
npm install
npm install react-router recharts
npm run dev

O site sobe em http://localhost:5173. Deixe o terminal aberto: ao salvar um arquivo, a página atualiza sozinha.

2.3 Estrutura de pastas

index.html          única página HTML do site
src/
  main.tsx          liga o React ao index.html
  App.tsx           menu e rotas
  types.ts          tipos dos dados da API
  pages/            uma página por rota
  components/       peças visuais reutilizáveis
  hooks/            lógica (estado + chamadas)
  services/         comunicação com a API
  storage/          dados salvos no navegador

Essa mesma separação será usada no dashboard do TermoLOG.


3. Conceitos essenciais

import { useEffect, useState } from 'react';

// Props descritas com interface: passar texto={42} vira erro no editor
interface BotaoProps { texto: string; onClick?: () => void; }

function Botao({ texto, onClick }: BotaoProps) {
  return <button className="botao" onClick={onClick}>{texto}</button>;
}

function Exemplo() {
  const [valor, setValor] = useState(0);            // [valor atual, função para alterar] = useState(inicial)
  const [itens, setItens] = useState<string[]>([]);

  useEffect(() => { setItens(['a', 'b']); }, []);  // roda depois de aparecer na tela; [] = só uma vez

  return (
    <div>
      <Botao texto={`Cliques: ${valor}`} onClick={() => setValor((v) => v + 1)} />
      {valor > 5 && <p>Mais de 5 cliques!</p>}      {/* só aparece se a condição for verdadeira */}
      <ul>{itens.map((i) => <li key={i}>{i}</li>)}</ul> {/* listas sempre com key única */}
    </div>
  );
}
HTML JSX
class className
onclick="..." onClick={funcao}
style="color: red" style={{ color: 'red' }}
<input> <input /> (toda tag fechada)
— {expressão} insere qualquer valor JS

Regra de ouro: nunca mexa no HTML direto (document.getElementById). Mude o estado e o React redesenha a tela.


4. Exemplo básico: componente reutilizável

// src/types.ts — o mesmo formato do JSON que a API devolve
export interface Tarefa {
  id: number;
  titulo: string;
  concluida: boolean;
  criada_em: string;
}
export type NovaTarefa = Pick<Tarefa, 'titulo'>;
// src/components/Contador.tsx
import { useState } from 'react';

interface ContadorProps { rotulo: string; passo?: number; }

export default function Contador({ rotulo, passo = 1 }: ContadorProps) {
  const [valor, setValor] = useState(0);
  return (
    <p>
      {rotulo}: {valor}{' '}
      <button onClick={() => setValor((v) => v - passo)}>-{passo}</button>
      <button onClick={() => setValor(0)}>Zerar</button>
      <button onClick={() => setValor((v) => v + passo)}>+{passo}</button>
    </p>
  );
}
// src/pages/Ola.tsx
import { useState } from 'react';
import Contador from '../components/Contador';

export default function Ola() {
  const [nome, setNome] = useState('');
  return (
    <>
      <input value={nome} onChange={(e) => setNome(e.target.value)} placeholder="Seu nome" />
      <h1>Olá, {nome || 'mundo'}!</h1>
      <Contador rotulo="Contador A" />
      <Contador rotulo="Contador B" passo={5} />
    </>
  );
}

Cada Contador tem seu próprio estado: mexer em um não altera o outro. É assim que os cartões de sala do TermoLOG vão funcionar.

setValor((v) => v + passo) calcula o novo valor a partir do anterior — a forma mais segura de atualizar estado.


5. Comunicação com o servidor (React + FastAPI)

5.1 API de teste

# backend-mock/main.py
from datetime import datetime
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()
# o site roda na porta 5173 e a API na 8000: sem CORS o navegador bloqueia as chamadas
app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:5173"],
                   allow_methods=["*"], allow_headers=["*"])

class NovaTarefa(BaseModel):
    titulo: str

class AtualizaTarefa(BaseModel):
    titulo: str | None = None
    concluida: bool | None = None

class Tarefa(NovaTarefa):
    id: int
    concluida: bool = False
    criada_em: datetime

tarefas: dict[int, Tarefa] = {}
proximo_id = 1

@app.get("/tarefas")
def listar():
    return list(tarefas.values())

@app.get("/tarefas/{id}")
def buscar(id: int):
    if id not in tarefas:
        raise HTTPException(404, "Tarefa não encontrada")
    return tarefas[id]

@app.post("/tarefas", status_code=201)
def criar(nova: NovaTarefa):
    global proximo_id
    t = Tarefa(id=proximo_id, titulo=nova.titulo, criada_em=datetime.now())
    tarefas[t.id] = t
    proximo_id += 1
    return t

@app.patch("/tarefas/{id}")
def atualizar(id: int, dados: AtualizaTarefa):
    tarefas[id] = buscar(id).model_copy(update=dados.model_dump(exclude_unset=True))
    return tarefas[id]

@app.delete("/tarefas/{id}", status_code=204)
def remover(id: int):
    buscar(id)
    del tarefas[id]
pip install fastapi uvicorn
uvicorn main:app --reload --port 8000

Em http://localhost:8000/docs o FastAPI mostra a documentação automática (Swagger) para testar cada rota.

5.2 Endereço da API

# .env (na raiz do projeto React)
VITE_API_URL=http://localhost:8000
  • A variável precisa começar com VITE_, senão o código não enxerga.
  • Após mudar o .env, reinicie o npm run dev.
  • Essas variáveis vão para o navegador do usuário: nunca coloque senhas nelas.

5.3 Serviço: todas as chamadas HTTP em um só lugar

// src/services/api.ts
import type { NovaTarefa, Tarefa } from '../types';

const BASE = import.meta.env.VITE_API_URL;

async function request<T>(caminho: string, init?: RequestInit): Promise<T> {
  const resp = await fetch(BASE + caminho, {
    headers: { 'Content-Type': 'application/json' },
    ...init,
  });
  if (!resp.ok) throw new Error(`Erro ${resp.status}`);
  return resp.status === 204 ? (undefined as T) : resp.json();
}

export const api = {
  listar: () => request<Tarefa[]>('/tarefas'),
  buscar: (id: number) => request<Tarefa>(`/tarefas/${id}`),
  criar: (nova: NovaTarefa) =>
    request<Tarefa>('/tarefas', { method: 'POST', body: JSON.stringify(nova) }),
  atualizar: (id: number, dados: Partial<Tarefa>) =>
    request<Tarefa>(`/tarefas/${id}`, { method: 'PATCH', body: JSON.stringify(dados) }),
  remover: (id: number) => request<void>(`/tarefas/${id}`, { method: 'DELETE' }),
};

O genérico <T> faz api.listar() devolver Tarefa[] já tipado: o editor sabe quais campos existem em todo o site.

5.4 Hook: estado + chamadas fora da página

// src/hooks/useTarefas.ts
import { useEffect, useState } from 'react';
import { api } from '../services/api';
import { lerCache, salvarCache } from '../storage/cache';
import type { Tarefa } from '../types';

export function useTarefas() {
  const [tarefas, setTarefas] = useState<Tarefa[]>([]);
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState<string | null>(null);

  useEffect(() => {
    api.listar()
      .then((lista) => { setTarefas(lista); salvarCache(lista); })
      .catch(() => {                                // API fora do ar: usa o cache (Seção 6)
        setTarefas(lerCache());
        setErro('Sem conexão — exibindo dados salvos no navegador');
      })
      .finally(() => setCarregando(false));
  }, []);

  // Persistência no servidor (Seção 7)
  async function adicionar(titulo: string) {
    const nova = await api.criar({ titulo });      // resposta já vem com o id do servidor
    setTarefas((atual) => [...atual, nova]);
  }
  async function alternar(t: Tarefa) {
    const atualizada = await api.atualizar(t.id, { concluida: !t.concluida });
    setTarefas((atual) => atual.map((x) => (x.id === t.id ? atualizada : x)));
  }
  async function remover(id: number) {
    await api.remover(id);
    setTarefas((atual) => atual.filter((x) => x.id !== id));
  }

  return { tarefas, carregando, erro, adicionar, alternar, remover };
}

5.5 Página de tarefas

// src/pages/Tarefas.tsx
import { useState, type FormEvent } from 'react';
import { Link } from 'react-router';
import { useTarefas } from '../hooks/useTarefas';
import { usePreferencias } from '../hooks/usePreferencias';

export default function Tarefas() {
  const { tarefas, carregando, erro, adicionar, alternar, remover } = useTarefas();
  const { prefs } = usePreferencias();
  const [titulo, setTitulo] = useState('');

  function enviar(e: FormEvent) {
    e.preventDefault();                            // sem isso o navegador recarrega a página
    if (titulo.trim()) adicionar(titulo).then(() => setTitulo(''));
  }

  if (carregando) return <p>Carregando...</p>;
  const visiveis = prefs.ocultarConcluidas ? tarefas.filter((t) => !t.concluida) : tarefas;

  return (
    <>
      {prefs.nome && <h2>Olá, {prefs.nome}!</h2>}
      {erro && <p style={{ background: '#fde68a' }}>{erro}</p>}
      <form onSubmit={enviar}>
        <input value={titulo} onChange={(e) => setTitulo(e.target.value)} />
        <button>Adicionar</button>
      </form>
      <ul>
        {visiveis.map((t) => (
          <li key={t.id}>
            <input type="checkbox" checked={t.concluida} onChange={() => alternar(t)} />
            <Link to={`/tarefa/${t.id}`}>{t.titulo}</Link>
            <button onClick={() => remover(t.id)}>🗑</button>
          </li>
        ))}
      </ul>
    </>
  );
}

No DevTools (F12) → aba Network dá para ver o GET /tarefas e o JSON de resposta. No TermoLOG será, por exemplo, GET /salas trazendo as temperaturas.


6. Persistência local (localStorage)

O localStorage já vem no navegador: um dicionário chave → texto que sobrevive ao fechar a aba. Operações: setItem, getItem, removeItem. Objetos são convertidos com JSON.stringify / JSON.parse.

// src/storage/preferencias.ts
export interface Preferencias { nome: string; ocultarConcluidas: boolean; }

const CHAVE = 'meusite:preferencias';

export function lerPreferencias(): Preferencias {
  const salvo = localStorage.getItem(CHAVE);
  return salvo ? JSON.parse(salvo) : { nome: '', ocultarConcluidas: false };
}
export function salvarPreferencias(p: Preferencias) {
  localStorage.setItem(CHAVE, JSON.stringify(p));
}
// src/storage/cache.ts — últimas tarefas recebidas, usadas se a API cair
import type { Tarefa } from '../types';

const CHAVE = 'meusite:cache-tarefas';
export const salvarCache = (t: Tarefa[]) => localStorage.setItem(CHAVE, JSON.stringify(t));
export const lerCache = (): Tarefa[] => JSON.parse(localStorage.getItem(CHAVE) ?? '[]');
// src/hooks/usePreferencias.ts
import { useState } from 'react';
import { lerPreferencias, salvarPreferencias, type Preferencias } from '../storage/preferencias';

export function usePreferencias() {
  const [prefs, setPrefs] = useState(lerPreferencias);   // já começa com o que estava salvo
  function atualizar(mudanca: Partial<Preferencias>) {
    const novas = { ...prefs, ...mudanca };
    setPrefs(novas);
    salvarPreferencias(novas);
  }
  return { prefs, atualizar };
}
// src/pages/Configuracoes.tsx
import { usePreferencias } from '../hooks/usePreferencias';

export default function Configuracoes() {
  const { prefs, atualizar } = usePreferencias();
  return (
    <>
      <input value={prefs.nome} onChange={(e) => atualizar({ nome: e.target.value })} placeholder="Seu nome" />
      <label>
        <input type="checkbox" checked={prefs.ocultarConcluidas}
               onChange={(e) => atualizar({ ocultarConcluidas: e.target.checked })} />
        Ocultar tarefas concluídas
      </label>
    </>
  );
}

Recarregue a página (F5): as preferências continuam lá. Em DevTools → Application → Local Storage dá para ver o que foi salvo. Parando a API e recarregando, a lista vem do cache com o aviso de "sem conexão" — no TermoLOG, o dashboard continua mostrando as últimas leituras se o servidor cair por um momento.


7. Persistência no servidor

Local Servidor
Onde fica Navegador Banco de dados
Quem vê Só este usuário Todos os usuários
Uso Preferências, cache Dados principais (limites de alarme, relatórios)
Como localStorage POST / PATCH / DELETE

O padrão (já implementado em adicionar, alternar e remover na Seção 5.4) é sempre: envia para o servidor → usa a resposta para atualizar o estado.

⚠️ Nunca altere o array do estado direto (tarefas.push(...)). Sempre crie um array novo com ..., map ou filter — é assim que o React percebe a mudança.

Para conferir, abra http://localhost:8000/tarefas: as alterações feitas no site estão lá, e aparecem iguais em qualquer navegador.


8. Navegação e gráficos

8.1 Rotas (React Router)

Um site React é uma SPA: o navegador carrega um único index.html e o React Router troca o componente conforme a URL, sem recarregar.

// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter><App /></BrowserRouter>
  </StrictMode>,
);
// src/App.tsx
import { NavLink, Route, Routes } from 'react-router';
import Tarefas from './pages/Tarefas';
import DetalheTarefa from './pages/DetalheTarefa';
import Estatisticas from './pages/Estatisticas';
import Configuracoes from './pages/Configuracoes';
import Ola from './pages/Ola';

export default function App() {
  return (
    <>
      <nav>  {/* NavLink marca sozinho o link da página atual */}
        <NavLink to="/">Tarefas</NavLink> | <NavLink to="/estatisticas">Estatísticas</NavLink> |{' '}
        <NavLink to="/configuracoes">Configurações</NavLink> | <NavLink to="/ola">Olá</NavLink>
      </nav>
      <Routes>
        <Route path="/" element={<Tarefas />} />
        <Route path="/tarefa/:id" element={<DetalheTarefa />} />  {/* rota dinâmica */}
        <Route path="/estatisticas" element={<Estatisticas />} />
        <Route path="/configuracoes" element={<Configuracoes />} />
        <Route path="/ola" element={<Ola />} />
        <Route path="*" element={<h1>404 — Página não encontrada</h1>} />
      </Routes>
    </>
  );
}
// src/pages/DetalheTarefa.tsx
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import { api } from '../services/api';

export default function DetalheTarefa() {
  const id = Number(useParams().id);               // parâmetro da URL sempre chega como texto
  const navigate = useNavigate();
  const [titulo, setTitulo] = useState('');
  const [msg, setMsg] = useState('');

  useEffect(() => { api.buscar(id).then((t) => setTitulo(t.titulo)); }, [id]);

  async function salvar() {
    await api.atualizar(id, { titulo });
    setMsg('Salvo no servidor!');
  }

  return (
    <>
      <input value={titulo} onChange={(e) => setTitulo(e.target.value)} />
      <button onClick={salvar}>Salvar</button>
      <button onClick={() => navigate(-1)}>Voltar</button>
      {msg && <p>{msg}</p>}
    </>
  );
}
  • Use <Link to="..."> em vez de <a href>: o <a> recarrega o site inteiro e o estado se perde.
  • useNavigate() navega pelo código (navigate(-1) volta uma página).
  • useParams() lê os parâmetros da URL.

8.2 Gráficos (Recharts)

No Recharts o gráfico é montado com componentes React: os dados são uma lista de objetos e o dataKey diz qual campo usar.

// src/pages/Estatisticas.tsx
import { Bar, BarChart, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { useTarefas } from '../hooks/useTarefas';

export default function Estatisticas() {
  const { tarefas } = useTarefas();

  const status = [
    { nome: 'Concluídas', valor: tarefas.filter((t) => t.concluida).length },
    { nome: 'Pendentes', valor: tarefas.filter((t) => !t.concluida).length },
  ];

  const contagem: Record<string, number> = {};
  for (const t of tarefas) {
    const dia = t.criada_em.slice(0, 10);
    contagem[dia] = (contagem[dia] ?? 0) + 1;
  }
  const porDia = Object.entries(contagem).map(([dia, total]) => ({ dia, total }));

  return (
    <>
      <ResponsiveContainer width="100%" height={250}>
        <PieChart>
          <Pie data={status} dataKey="valor" nameKey="nome" label>
            <Cell fill="#22c55e" />
            <Cell fill="#f59e0b" />
          </Pie>
          <Tooltip />
        </PieChart>
      </ResponsiveContainer>
      <ResponsiveContainer width="100%" height={250}>
        <BarChart data={porDia}>
          <XAxis dataKey="dia" />
          <YAxis allowDecimals={false} />
          <Tooltip />
          <Bar dataKey="total" fill="#3b82f6" />
        </BarChart>
      </ResponsiveContainer>
    </>
  );
}

No TermoLOG, o mesmo recurso vira um LineChart com a temperatura de cada sala ao longo do tempo.


9. Problemas comuns

Sintoma Solução
Erro de CORS no console Habilitar o CORSMiddleware na API
Mudou o .env e nada aconteceu Reiniciar o npm run dev
Variável de ambiente undefined Faltou o prefixo VITE_
Aviso de key em lista key={item.id} em cada item
Tela não atualiza Usar useState e criar um array/objeto novo
Página recarrega ao clicar num link Trocar <a href> por <Link to>

Para publicar: npm run build verifica os tipos e gera a pasta dist/, que pode ser hospedada em qualquer servidor estático.


Para aprofundar

Clone this wiki locally