Neste codelab, um arquivo chamado produtos.ts será usado como uma fonte local de dados. Ele terá uma lista de produtos escrita em TypeScript e será importado diretamente pelas páginas React.

O comportamento lembrará o consumo de uma API porque teremos:

Porém, existe uma diferença fundamental: o arquivo já faz parte do projeto. Não haverá requisição de rede, tempo de espera, carregamento externo ou programação assíncrona.

Visão geral do caminho percorrido pelos dados

Etapa

Arquivo ou componente

O que acontece

1

produto.ts

Define o formato obrigatório de um produto.

2

produtos.ts

Cria e exporta o array listaProdutos.

3

routes/Produtos/index.tsx

Importa o array, aplica filtros e prepara a listagem.

4

CardProduto.tsx

Recebe um produto por props e apresenta seus dados.

5

Botão "Editar produto"

Informa o id do produto selecionado.

6

useNavigate

Abre /editar-produtos/ID.

7

routes/EditarProdutos/index.tsx

Lê o id da URL e procura o objeto correspondente.

8

FormularioEdicao

Recebe o produto encontrado e apresenta os campos preenchidos.

Projeto considerado

O projeto já existe e se chama:

my-app

As seguintes rotas já estão configuradas:

URL

Componente

/

routes/Home/index.tsx

/produtos

routes/Produtos/index.tsx

/editar-produtos/:id

routes/EditarProdutos/index.tsx

erro de rota

routes/Error/index.tsx, por meio de errorElement

Este material preservará exatamente essa convenção: nomes das pastas de componentes com inicial maiúscula, arquivos index.tsx e URLs em letras minúsculas.

O que você aprenderá

Ao final, você conseguirá:

Forma de estudo

Em cada etapa:

  1. entenda a ideia;
  2. leia o código;
  3. digite uma parte de cada vez;
  4. preveja o resultado;
  5. teste no navegador;
  6. compare com a explicação;
  7. pratique antes de avançar.

Antes de escrever código, precisamos separar duas ideias.

Fonte local de dados

Quando importamos um arquivo TypeScript:

import { listaProdutos } from "../../data/produtos";

o Vite encontra esse módulo durante o desenvolvimento e durante a construção do projeto. O array exportado passa a fazer parte da aplicação.

O fluxo é direto:

produtos.ts
    ↓ import
routes/Produtos/index.tsx
    ↓ props
CardProduto.tsx

API verdadeira

Em uma API verdadeira, a aplicação normalmente precisa solicitar dados a outro endereço:

React → rede → servidor → banco de dados → resposta → React

Esse processo pode demorar, falhar ou depender de autenticação. Nada disso acontece neste exercício.

Comparação

Arquivo .ts local

API verdadeira

Faz parte do projeto.

Está em outro serviço ou servidor.

É importado diretamente.

É solicitado pela rede.

Os dados chegam imediatamente ao módulo.

A resposta pode demorar.

Não possui erro HTTP.

Pode devolver 404, 500 e outros códigos.

Alterar a tela não altera o arquivo-fonte.

O servidor pode persistir alterações.

Excelente para aprendizado e protótipos.

Necessária para dados compartilhados e persistentes.

"Como se fosse uma API" significa o quê?

Significa que organizaremos o projeto como se existisse uma fonte central de dados. As páginas não criarão objetos espalhados pelo JSX. Elas receberão os dados de um arquivo especializado.

Não significa que o arquivo local ganhou recursos de servidor.

Verificação de compreensão

Responda antes de continuar:

  1. O navegador precisa aguardar uma resposta de rede para importar produtos.ts?
  2. Alterar um campo do formulário modifica fisicamente o arquivo?
  3. Por que ainda é útil centralizar os dados?

O projeto final utilizará esta organização:

my-app/
├── public/
├── src/
│   ├── components/
│   │   ├── Cabecalho.tsx
│   │   ├── CardProduto.tsx
│   │   └── Rodape.tsx
│   ├── data/
│   │   └── produtos.ts
│   ├── routes/
│   │   ├── EditarProdutos/
│   │   │   └── index.tsx
│   │   ├── Error/
│   │   │   └── index.tsx
│   │   ├── Home/
│   │   │   └── index.tsx
│   │   └── Produtos/
│   │       └── index.tsx
│   ├── types/
│   │   └── produto.ts
│   ├── App.tsx
│   └── main.tsx
├── package.json
└── vite.config.ts

Responsabilidade de cada pasta

Pasta

Responsabilidade

components

Componentes reutilizáveis, como o card.

data

Fonte local de dados.

routes

Componentes relacionados às URLs, cada um em uma pasta com index.tsx.

types

Contratos TypeScript compartilhados.

Por que separar types e data?

O arquivo de tipos responde:

Qual é o formato válido de um produto?

O arquivo de dados responde:

Quais produtos existem neste exemplo?

Essa separação permite que páginas, componentes e dados utilizem o mesmo contrato.

Antes de criar arquivos

No terminal, entre no projeto e confirme que ele funciona:

cd my-app
npm run dev

Abra o endereço mostrado pelo Vite e teste as rotas existentes.

Ponto de conferência das rotas

Você não precisa recriar o roteamento se ele já funciona. Apenas confira se o arquivo src/main.tsx segue a estrutura ensinada:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router";

import App from "./App.tsx";
import EditarProdutos from "./routes/EditarProdutos/index.tsx";
import Error from "./routes/Error/index.tsx";
import Home from "./routes/Home/index.tsx";
import Produtos from "./routes/Produtos/index.tsx";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    errorElement: <Error />,
    children: [
      {
        path: "/",
        element: <Home />,
      },
      {
        path: "/produtos",
        element: <Produtos />,
      },
      {
        path: "/editar-produtos/:id",
        element: <EditarProdutos />,
      },
    ],
  },
]);

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>,
);

Como as páginas são filhas de App, o componente App.tsx precisa possuir um Outlet:

import { Outlet } from "react-router";
import Cabecalho from "./components/Cabecalho";
import Rodape from "./components/Rodape";

export default function App() {
  return (
    <div>
      <Cabecalho />
      <Outlet />
      <Rodape />
    </div>
  );
}

Outlet é o espaço reservado no layout para a rota filha correspondente à URL atual. Cabecalho e Rodape permanecem ao redor de todas as páginas.

Crie:

src/types/produto.ts

Digite:

export type CategoriaProduto =
  | "notebook"
  | "smartphone"
  | "acessorio";

export type StatusProduto =
  | "disponivel"
  | "baixo-estoque"
  | "indisponivel";

export interface Produto {
  id: number;
  nome: string;
  preco: number;
  categoria: CategoriaProduto;
  status: StatusProduto;
  estoque: number;
  imagem: string;
  descricao: string;
}

export type FiltroCategoria = "todos" | CategoriaProduto;

export type FormularioProduto = Omit<Produto, "id">;

Primeiro: o que type faz?

type cria um nome para uma definição de tipo.

type CategoriaProduto = "notebook" | "smartphone" | "acessorio";

Não estamos criando uma variável chamada CategoriaProduto. Estamos criando uma regra usada pelo TypeScript.

União de tipos

O símbolo | significa ou.

"notebook" OU "smartphone" OU "acessorio"

Isto é válido:

const categoria: CategoriaProduto = "notebook";

Isto produz erro:

const categoria: CategoriaProduto = "televisao";

O erro aparece porque "televisao" não faz parte das opções permitidas.

O que é uma interface?

A interface descreve o formato esperado de um objeto.

export interface Produto {
  id: number;
  nome: string;
}

Ela diz que todo produto precisa ter:

Leia cada propriedade

Propriedade

Tipo

Significado

id

number

Identificador único.

nome

string

Nome apresentado na tela.

preco

number

Valor numérico usado em cálculos e formatação.

categoria

CategoriaProduto

Uma das categorias permitidas.

status

StatusProduto

Situação comercial do produto.

estoque

number

Quantidade disponível.

imagem

string

Caminho ou URL da imagem.

descricao

string

Texto explicativo.

Ponto e vírgula dentro da interface

Em:

nome: string;

Entendendo FiltroCategoria

export type FiltroCategoria = "todos" | CategoriaProduto;

Isso equivale a permitir:

"todos" | "notebook" | "smartphone" | "acessorio"

Entendendo Omit

export type FormularioProduto = Omit<Produto, "id">;

Omit cria um novo tipo retirando propriedades de outro tipo.

Aqui, o formulário terá todos os campos de Produto, menos id.

O id fica de fora porque identifica o produto que já existe. O usuário editará nome, preço, estoque e demais informações, mas não trocará sua identidade.

Exercício diagnóstico

Quais linhas são válidas?

const a: CategoriaProduto = "smartphone";
const b: CategoriaProduto = "monitor";
const c: StatusProduto = "baixo-estoque";
const d: StatusProduto = "acabando";

Crie:

src/data/produtos.ts

Digite:

import type { Produto } from "../types/produto";

export const listaProdutos: Produto[] = [
  {
    id: 1,
    nome: "Notebook Pro 15",
    preco: 5799.9,
    categoria: "notebook",
    status: "disponivel",
    estoque: 12,
    imagem: "https://placehold.co/600x400/172554/ffffff?text=Notebook+Pro+15",
    descricao: "Notebook para desenvolvimento, estudos e produtividade.",
  },
  {
    id: 2,
    nome: "Smartphone Connect X",
    preco: 2899.9,
    categoria: "smartphone",
    status: "baixo-estoque",
    estoque: 4,
    imagem: "https://placehold.co/600x400/14532d/ffffff?text=Connect+X",
    descricao: "Smartphone com tela de alta resolução e conexão 5G.",
  },
  {
    id: 3,
    nome: "Mouse Precision",
    preco: 189.9,
    categoria: "acessorio",
    status: "disponivel",
    estoque: 30,
    imagem: "https://placehold.co/600x400/78350f/ffffff?text=Mouse+Precision",
    descricao: "Mouse ergonômico com seis botões configuráveis.",
  },
  {
    id: 4,
    nome: "Notebook Air 13",
    preco: 4299.9,
    categoria: "notebook",
    status: "baixo-estoque",
    estoque: 3,
    imagem: "https://placehold.co/600x400/581c87/ffffff?text=Notebook+Air+13",
    descricao: "Notebook leve para mobilidade e tarefas acadêmicas.",
  },
  {
    id: 5,
    nome: "Headset Focus",
    preco: 349.9,
    categoria: "acessorio",
    status: "indisponivel",
    estoque: 0,
    imagem: "https://placehold.co/600x400/7f1d1d/ffffff?text=Headset+Focus",
    descricao: "Headset com microfone e isolamento acústico.",
  },
  {
    id: 6,
    nome: "Smartphone Start",
    preco: 1599.9,
    categoria: "smartphone",
    status: "disponivel",
    estoque: 18,
    imagem: "https://placehold.co/600x400/164e63/ffffff?text=Smartphone+Start",
    descricao: "Smartphone para comunicação, estudos e tarefas diárias.",
  },
];

Importação somente de tipo

import type { Produto } from "../types/produto";

import type informa que Produto será usado somente pelo TypeScript para conferência.

Ele não representa um valor utilizado pelo navegador.

Exportação nomeada

export const listaProdutos = ...

O export permite que outro módulo importe listaProdutos pelo mesmo nome.

O que significa Produto[]?

const listaProdutos: Produto[] = [];

Os colchetes [] depois de Produto significam:

array cujos itens devem obedecer à interface Produto.

O TypeScript verificará cada objeto do array.

Teste um erro proposital

Altere temporariamente:

preco: 5799.9,

para:

preco: "5799.90",

O editor deverá informar que string não pode ser atribuída a number.

Depois, desfaça a alteração.

Por que o preço não contém R$?

Isto está correto:

preco: 5799.9,

Isto dificultaria cálculos:

preco: "R$ 5.799,90",

O dado permanece numérico. A formatação em reais será feita no componente.

Por que cada id deve ser único?

O id será usado para:

Dois produtos com o mesmo id criariam ambiguidade.

Abra:

src/routes/Produtos/index.tsx

Comece com uma versão mínima:

import { listaProdutos } from "../../data/produtos";

export default function Produtos() {
  console.table(listaProdutos);

  return (
    <main className="mx-auto max-w-7xl px-4 py-10">
      <h1 className="text-3xl font-bold text-slate-900">
        Produtos
      </h1>

      <p className="mt-2 text-slate-600">
        Quantidade cadastrada: {listaProdutos.length}
      </p>
    </main>
  );
}

Leia a importação

import { listaProdutos } from "../../data/produtos";

Trecho

Explicação

import

Solicita algo exportado por outro módulo.

{ listaProdutos }

Nome exato da exportação nomeada.

from

Indica de onde vem o valor.

../../

Sobe de routes/Produtos para src.

data/produtos

Entra em data e encontra produtos.ts.

Não é necessário escrever .ts no final do caminho.

console.table

console.table(listaProdutos);

Esse comando apresenta arrays de objetos em formato de tabela no console do navegador.

Para conferir:

  1. abra /produtos;
  2. pressione F12;
  3. abra a guia Console;
  4. encontre a tabela;
  5. confirme as propriedades e os seis itens.

length

{listaProdutos.length}

length informa a quantidade de itens do array. Não é uma função, por isso não recebe parênteses.

O ponto mais importante

Não precisamos de um hook para "carregar" essa lista. Quando o módulo Produtos.tsx é executado, listaProdutos já foi importada.

map percorre um array e cria um novo array com o resultado produzido para cada item.

const nomes = listaProdutos.map((produto) => produto.nome);

Entrada simplificada:

[
  { nome: "Notebook Pro 15" },
  { nome: "Mouse Precision" }
]

Saída:

[
  "Notebook Pro 15",
  "Mouse Precision"
]

No React, o novo array pode conter elementos JSX:

<ul>
  {listaProdutos.map((produto) => (
    <li key={produto.id}>{produto.nome}</li>
  ))}
</ul>

Leia linha por linha

listaProdutos.map((produto) => (
  <li key={produto.id}>{produto.nome}</li>
))
  1. listaProdutos é o array original.
  2. .map pede que cada item seja transformado.
  3. produto é o item atual da repetição.
  4. => separa a entrada da transformação.
  5. O JSX entre parênteses é devolvido para cada produto.
  6. O resultado é um array de elementos li.

Por que usar key?

key={produto.id}

A key ajuda o React a identificar cada item entre renderizações.

Uma boa key deve ser:

O id cumpre essas condições.

Erro comum: usar chaves sem devolver JSX

Isto não devolve nada:

listaProdutos.map((produto) => {
  <li>{produto.nome}</li>;
});

Com chaves, escreva return:

listaProdutos.map((produto) => {
  return <li key={produto.id}>{produto.nome}</li>;
});

Ou use parênteses para retorno direto:

listaProdutos.map((produto) => (
  <li key={produto.id}>{produto.nome}</li>
));

Crie:

src/components/CardProduto.tsx

Digite:

import type { Produto } from "../types/produto";

interface CardProdutoProps {
  produto: Produto;
  onEditar: (id: number) => void;
}

export default function CardProduto({
  produto,
  onEditar,
}: CardProdutoProps) {
  const precoFormatado = produto.preco.toLocaleString("pt-BR", {
    style: "currency",
    currency: "BRL",
  });

  return (
    <article className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
      <img
        src={produto.imagem}
        alt={`Imagem ilustrativa de ${produto.nome}`}
        className="h-48 w-full object-cover"
      />

      <div className="space-y-3 p-5">
        <div className="flex items-start justify-between gap-3">
          <h2 className="text-xl font-semibold text-slate-900">
            {produto.nome}
          </h2>

          <span className="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700">
            {produto.categoria}
          </span>
        </div>

        <p className="text-sm leading-6 text-slate-600">
          {produto.descricao}
        </p>

        <dl className="grid grid-cols-2 gap-3 text-sm">
          <div>
            <dt className="text-slate-500">Preço</dt>
            <dd className="font-semibold text-slate-900">
              {precoFormatado}
            </dd>
          </div>

          <div>
            <dt className="text-slate-500">Estoque</dt>
            <dd className="font-semibold text-slate-900">
              {produto.estoque} unidades
            </dd>
          </div>
        </dl>

        <p className="text-sm font-medium text-slate-700">
          Status: {produto.status}
        </p>

        <button
          type="button"
          onClick={() => onEditar(produto.id)}
          className="w-full rounded-xl bg-slate-900 px-4 py-3 font-semibold text-white hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
        >
          Editar produto
        </button>
      </div>
    </article>
  );
}

O componente recebe duas props

interface CardProdutoProps {
  produto: Produto;
  onEditar: (id: number) => void;
}

Prop

Tipo

Responsabilidade

produto

Produto

Fornece os dados exibidos.

onEditar

função

Avisa ao componente pai qual id deve ser editado.

Leia o tipo da função

onEditar: (id: number) => void;

Desestruturação das props

function CardProduto({ produto, onEditar }: CardProdutoProps)

Sem desestruturação, seria necessário escrever:

function CardProduto(props: CardProdutoProps) {
  props.produto.nome;
  props.onEditar(props.produto.id);
}

Com desestruturação, usamos diretamente produto e onEditar.

Formatação do preço

produto.preco.toLocaleString("pt-BR", {
  style: "currency",
  currency: "BRL",
});

O valor continua numérico nos dados. O componente apenas cria uma representação apropriada para reais brasileiros.

Clique no botão

onClick={() => onEditar(produto.id)}

A função não é executada durante a renderização. Ela será executada quando o botão for acionado.

Não escreva:

onClick={onEditar(produto.id)}

Essa forma tentaria executar onEditar imediatamente.

Atualize temporariamente routes/Produtos/index.tsx:

import CardProduto from "../../components/CardProduto";
import { listaProdutos } from "../../data/produtos";

export default function Produtos() {
  function testarEdicao(id: number) {
    console.log("Produto escolhido:", id);
  }

  return (
    <main className="mx-auto max-w-7xl px-4 py-10">
      <h1 className="text-3xl font-bold text-slate-900">
        Produtos
      </h1>

      <div className="mt-8 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
        {listaProdutos.map((produto) => (
          <CardProduto
            key={produto.id}
            produto={produto}
            onEditar={testarEdicao}
          />
        ))}
      </div>
    </main>
  );
}

O que o map entrega ao card?

Em cada repetição:

produto={produto}

O primeiro produto é o nome da prop. O segundo é a variável da repetição.

Podemos ler como:

Entregue à prop chamada produto o objeto do produto atual.

A função também é enviada

onEditar={testarEdicao}

Não há parênteses porque estamos passando a função para o componente filho. O card decidirá quando executá-la.

Teste

  1. abra /produtos;
  2. confira os seis cards;
  3. abra o console;
  4. clique em diferentes botões;
  5. confirme que cada botão mostra um id diferente.

Agora a tela terá informações que mudam durante a interação.

No início de routes/Produtos/index.tsx, importe:

import { useState } from "react";
import type { FiltroCategoria } from "../../types/produto";

Dentro do componente:

const [busca, setBusca] = useState("");

const [categoria, setCategoria] =
  useState<FiltroCategoria>("todos");

O que é estado?

Estado é uma informação que:

Leia useState

const [busca, setBusca] = useState("");

Parte

Significado

busca

Valor atual.

setBusca

Função usada para atualizar o valor.

useState("")

Cria o estado com texto vazio.

[ ]

Desestruturação do array devolvido pelo hook.

Estado tipado da categoria

useState<FiltroCategoria>("todos")

O trecho informa os valores aceitos pelo estado.

Crie os controles

<section
  aria-label="Filtros dos produtos"
  className="mt-8 grid gap-4 rounded-2xl bg-slate-100 p-5 md:grid-cols-2"
>
  <div>
    <label
      htmlFor="busca"
      className="mb-2 block font-medium text-slate-800"
    >
      Buscar por nome ou descrição
    </label>

    <input
      id="busca"
      type="search"
      value={busca}
      onChange={(event) => setBusca(event.target.value)}
      className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-slate-900"
      placeholder="Exemplo: notebook"
    />
  </div>

  <div>
    <label
      htmlFor="categoria"
      className="mb-2 block font-medium text-slate-800"
    >
      Categoria
    </label>

    <select
      id="categoria"
      value={categoria}
      onChange={(event) =>
        setCategoria(event.target.value as FiltroCategoria)
      }
      className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3 text-slate-900"
    >
      <option value="todos">Todos</option>
      <option value="notebook">Notebooks</option>
      <option value="smartphone">Smartphones</option>
      <option value="acessorio">Acessórios</option>
    </select>
  </div>
</section>

Campo controlado

O input recebe o valor do estado:

value={busca}

E atualiza o estado quando a pessoa digita:

onChange={(event) => setBusca(event.target.value)}

Esse conjunto forma um campo controlado.

Por que existe as FiltroCategoria?

Para o TypeScript, event.target.value é uma string genérica. Porém, sabemos que as opções do select pertencem a FiltroCategoria.

event.target.value as FiltroCategoria

Essa afirmação só é segura porque controlamos as opções do select.

Agora criaremos uma lista derivada.

Dentro de Produtos, depois dos estados:

const termoNormalizado = busca.trim().toLowerCase();

const produtosFiltrados = listaProdutos.filter((produto) => {
  const nomeNormalizado = produto.nome.toLowerCase();
  const descricaoNormalizada = produto.descricao.toLowerCase();

  const correspondeAoTexto =
    nomeNormalizado.includes(termoNormalizado) ||
    descricaoNormalizada.includes(termoNormalizado);

  const correspondeACategoria =
    categoria === "todos" || produto.categoria === categoria;

  return correspondeAoTexto && correspondeACategoria;
});

O que filter devolve?

filter cria um novo array contendo apenas os itens aprovados pela condição.

Para cada produto, a função precisa devolver:

trim

busca.trim()

Remove espaços no início e no final.

"  notebook  " → "notebook"

toLowerCase

produto.nome.toLowerCase()

Cria uma versão em letras minúsculas.

"Notebook Pro 15" → "notebook pro 15"

Isso permite comparar textos sem depender de maiúsculas e minúsculas.

includes

nomeNormalizado.includes(termoNormalizado)

Pergunta se um texto contém outro texto.

"notebook pro 15" contém "pro"? true
"mouse precision" contém "pro"? false

Operador ||

nomeContem || descricaoContem

Significa ou. Basta uma das condições ser verdadeira.

Operador &&

correspondeAoTexto && correspondeACategoria

Significa e. As duas condições precisam ser verdadeiras.

Por que produtosFiltrados não é outro estado?

Ele pode ser calculado usando:

Como é um valor derivado, não precisamos duplicá-lo em outro useState.

Duplicar dados aumentaria o risco de manter estados desencontrados.

Renderize a lista filtrada

Troque:

listaProdutos.map(...)

por:

produtosFiltrados.map(...)

Adicione também:

<p className="mt-5 text-sm text-slate-600" aria-live="polite">
  {produtosFiltrados.length} produto(s) encontrado(s).
</p>

Testes obrigatórios

  1. Digite notebook.
  2. Apague e digite FOCUS.
  3. Digite espaços antes e depois de mouse.
  4. Selecione apenas acessórios.
  5. Combine busca e categoria.
  6. Digite algo inexistente.

Método

Pergunta principal

Resultado

map

O que cada item deve virar?

Outro array com a mesma quantidade.

filter

Quais itens devem permanecer?

Outro array com zero, um ou vários itens.

find

Qual é o primeiro item que atende à condição?

Um item ou undefined.

Exemplos com o mesmo array

const nomes = listaProdutos.map((produto) => produto.nome);

Resultado: array de nomes.

const acessorios = listaProdutos.filter(
  (produto) => produto.categoria === "acessorio",
);

Resultado: array de acessórios.

const produtoTres = listaProdutos.find(
  (produto) => produto.id === 3,
);

Resultado: o objeto de id 3 ou undefined.

Por que find será usado na edição?

A página de edição precisa de um produto específico, não de uma lista.

Por isso, find combina melhor com o problema.

Na página Produtos, importe:

import { useNavigate } from "react-router";

Dentro do componente:

const navigate = useNavigate();

Crie a função:

function abrirEdicao(id: number) {
  navigate(`/editar-produtos/${id}`);
}

Entregue essa função ao card:

<CardProduto
  key={produto.id}
  produto={produto}
  onEditar={abrirEdicao}
/>

O que o hook devolve?

useNavigate devolve uma função de navegação programática.

const navigate = useNavigate();

Não é uma página, um endereço ou um componente. É uma função que pode mudar a localização atual da aplicação.

Interpolação de texto

`/editar-produtos/${id}`

As crases criam uma template string. O trecho ${id} insere o valor do identificador.

Se id for 4, o endereço será:

/editar-produtos/4

O que é o segmento /4?

É o valor que ocupa o parâmetro dinâmico :id definido na rota.

/editar-produtos/:id  → definição da rota
/editar-produtos/4    → endereço acessado
                  └─ valor de id

Na configuração, :id funciona como um espaço reservado. Na navegação, esse espaço recebe o identificador do produto.

Por que usar o id e não o objeto inteiro?

Teste antes de criar a página final

  1. clique no botão do produto 1;
  2. observe /editar-produtos/1;
  3. volte;
  4. clique no produto 5;
  5. observe /editar-produtos/5.

Abra:

src/routes/EditarProdutos/index.tsx

Comece com:

import { Link, useParams } from "react-router";
import { listaProdutos } from "../../data/produtos";

export default function EditarProdutos() {
  const { id } = useParams();
  const produtoId = Number(id);

  const produtoEncontrado = listaProdutos.find(
    (produto) => produto.id === produtoId,
  );

  if (!produtoEncontrado) {
    return (
      <main className="mx-auto max-w-4xl px-4 py-10">
        <h1 className="text-3xl font-bold text-slate-900">
          Produto não encontrado
        </h1>
        <p className="mt-3 text-slate-600">
          Não existe um produto com o identificador informado.
        </p>
        <Link
          to="/produtos"
          className="mt-6 inline-block font-semibold text-blue-700 underline"
        >
          Voltar para produtos
        </Link>
      </main>
    );
  }

  return (
    <main className="mx-auto max-w-4xl px-4 py-10">
      <h1 className="text-3xl font-bold text-slate-900">
        Editar produto
      </h1>

      <p className="mt-3 text-slate-600">
        Produto encontrado: {produtoEncontrado.nome}
      </p>
    </main>
  );
}

Leia o hook

const { id } = useParams();

O hook devolve um objeto com os parâmetros dinâmicos da rota atual. Como a rota possui :id, desestruturamos a propriedade id.

Lendo id

const { id } = useParams();

Se a URL for:

/editar-produtos/4

id será o texto "4". Seu tipo também admite undefined, pois o TypeScript não pressupõe que todo endereço possua esse parâmetro.

A URL fornece texto

O id do nosso produto é number, mas o valor da URL chega como texto.

Por isso fazemos:

const produtoId = Number(id);

Exemplo:

"4" → 4
string → number

Localização com find

const produtoEncontrado = listaProdutos.find(
  (produto) => produto.id === produtoId,
);

O método percorre os produtos e para quando encontra o primeiro id igual.

O tipo inferido será:

Produto | undefined

Ou existe um produto, ou nada foi encontrado.

Proteção antes de usar o objeto

if (!produtoEncontrado) {
  return <main>Produto não encontrado.</main>;
}

Depois desse if, o TypeScript entende que produtoEncontrado existe. Neste caso, mostramos uma mensagem dentro da própria página. O componente Error configurado como errorElement continua responsável por URLs que não correspondem às rotas.

Testes de erro

Digite manualmente:

/editar-produtos/1
/editar-produtos/abc
/editar-produtos/999
/editar-produtos

O primeiro endereço deve localizar o produto 1. Os dois seguintes entram na página de edição e mostram "Produto não encontrado". O último não corresponde à rota /editar-produtos/:id; por isso, o roteador apresenta o componente definido em errorElement.

Não criaremos o estado do formulário antes de saber se o produto existe.

Uma solução clara é separar responsabilidades:

export default function EditarProdutos() {
  // Lê a URL e encontra o produto.

  if (!produtoEncontrado) {
    return <main>Produto não encontrado.</main>;
  }

  return <FormularioEdicao produto={produtoEncontrado} />;
}

O componente FormularioEdicao será responsável pelos hooks do formulário.

Por que não chamar useState depois de um retorno condicional?

Hooks precisam ser chamados sempre na mesma ordem em cada renderização do componente.

Evite:

if (!produtoEncontrado) {
  return <main>Produto não encontrado.</main>;
}

// Evite colocar o hook aqui no mesmo componente.
const [formulario, setFormulario] = useState(...);

Ao mover o formulário para outro componente:

Props do formulário

interface FormularioEdicaoProps {
  produto: Produto;
}

O formulário só será renderizado quando receber um produto válido.

No mesmo arquivo routes/EditarProdutos/index.tsx, adicione os imports:

import { useState, type FormEvent } from "react";
import type {
  FormularioProduto,
  Produto,
} from "../../types/produto";

Depois do componente principal, crie:

interface FormularioEdicaoProps {
  produto: Produto;
}

function FormularioEdicao({ produto }: FormularioEdicaoProps) {
  const [formulario, setFormulario] = useState<FormularioProduto>(() => ({
    nome: produto.nome,
    preco: produto.preco,
    categoria: produto.categoria,
    status: produto.status,
    estoque: produto.estoque,
    imagem: produto.imagem,
    descricao: produto.descricao,
  }));

  const [mensagem, setMensagem] = useState("");

  function salvarEdicao(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const produtoAtualizado: Produto = {
      id: produto.id,
      ...formulario,
    };

    console.table(produtoAtualizado);
    setMensagem("Alterações preparadas apenas na memória do navegador.");
  }

  return (
    <form
      onSubmit={salvarEdicao}
      className="mt-8 space-y-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
    >
      {/* Os campos serão adicionados na próxima etapa. */}

      <button
        type="submit"
        className="rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white hover:bg-slate-700"
      >
        Preparar alteração
      </button>

      <p aria-live="polite" className="text-sm font-medium text-emerald-700">
        {mensagem}
      </p>
    </form>
  );
}

Inicialização preguiçosa

useState<FormularioProduto>(() => ({ ... }))

O useState recebe uma função que cria o valor inicial. Essa função é usada na inicialização do estado.

Por que copiar os campos?

Não devemos alterar diretamente o objeto importado.

O formulário recebe sua própria cópia dos valores editáveis:

nome: produto.nome,
preco: produto.preco,

O operador de espalhamento

const produtoAtualizado = {
  id: produto.id,
  ...formulario,
};

...formulario espalha as propriedades do formulário dentro do novo objeto.

O resultado volta a possuir todas as propriedades de Produto, incluindo id.

Evento do formulário

event.preventDefault();

Impede o comportamento HTML padrão de recarregar ou navegar ao enviar o formulário.

Mensagem acessível

<p aria-live="polite">{mensagem}</p>

aria-live="polite" permite que tecnologias assistivas anunciem a mensagem quando ela mudar.

Substitua o retorno temporário de FormularioEdicao pelo formulário completo:

return (
  <form
    onSubmit={salvarEdicao}
    className="mt-8 space-y-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
  >
    <div>
      <label htmlFor="nome" className="mb-2 block font-medium text-slate-800">
        Nome
      </label>
      <input
        id="nome"
        type="text"
        value={formulario.nome}
        onChange={(event) =>
          setFormulario((estadoAtual) => ({
            ...estadoAtual,
            nome: event.target.value,
          }))
        }
        className="w-full rounded-xl border border-slate-300 px-4 py-3"
        required
      />
    </div>

    <div className="grid gap-6 md:grid-cols-2">
      <div>
        <label htmlFor="preco" className="mb-2 block font-medium text-slate-800">
          Preço
        </label>
        <input
          id="preco"
          type="number"
          min="0"
          step="0.01"
          value={formulario.preco}
          onChange={(event) =>
            setFormulario((estadoAtual) => ({
              ...estadoAtual,
              preco: Number(event.target.value),
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
          required
        />
      </div>

      <div>
        <label htmlFor="estoque" className="mb-2 block font-medium text-slate-800">
          Estoque
        </label>
        <input
          id="estoque"
          type="number"
          min="0"
          step="1"
          value={formulario.estoque}
          onChange={(event) =>
            setFormulario((estadoAtual) => ({
              ...estadoAtual,
              estoque: Number(event.target.value),
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
          required
        />
      </div>
    </div>

    <div className="grid gap-6 md:grid-cols-2">
      <div>
        <label htmlFor="categoria" className="mb-2 block font-medium text-slate-800">
          Categoria
        </label>
        <select
          id="categoria"
          value={formulario.categoria}
          onChange={(event) =>
            setFormulario((estadoAtual) => ({
              ...estadoAtual,
              categoria: event.target.value as Produto["categoria"],
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
        >
          <option value="notebook">Notebook</option>
          <option value="smartphone">Smartphone</option>
          <option value="acessorio">Acessório</option>
        </select>
      </div>

      <div>
        <label htmlFor="status" className="mb-2 block font-medium text-slate-800">
          Status
        </label>
        <select
          id="status"
          value={formulario.status}
          onChange={(event) =>
            setFormulario((estadoAtual) => ({
              ...estadoAtual,
              status: event.target.value as Produto["status"],
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
        >
          <option value="disponivel">Disponível</option>
          <option value="baixo-estoque">Baixo estoque</option>
          <option value="indisponivel">Indisponível</option>
        </select>
      </div>
    </div>

    <div>
      <label htmlFor="imagem" className="mb-2 block font-medium text-slate-800">
        Endereço da imagem
      </label>
      <input
        id="imagem"
        type="url"
        value={formulario.imagem}
        onChange={(event) =>
          setFormulario((estadoAtual) => ({
            ...estadoAtual,
            imagem: event.target.value,
          }))
        }
        className="w-full rounded-xl border border-slate-300 px-4 py-3"
        required
      />
    </div>

    <div>
      <label htmlFor="descricao" className="mb-2 block font-medium text-slate-800">
        Descrição
      </label>
      <textarea
        id="descricao"
        rows={5}
        value={formulario.descricao}
        onChange={(event) =>
          setFormulario((estadoAtual) => ({
            ...estadoAtual,
            descricao: event.target.value,
          }))
        }
        className="w-full rounded-xl border border-slate-300 px-4 py-3"
        required
      />
    </div>

    <figure className="overflow-hidden rounded-2xl bg-slate-100 p-4">
      <img
        src={formulario.imagem}
        alt={`Prévia de ${formulario.nome}`}
        className="mx-auto max-h-72 rounded-xl object-contain"
      />
      <figcaption className="mt-2 text-center text-sm text-slate-600">
        Prévia da imagem informada
      </figcaption>
    </figure>

    <button
      type="submit"
      className="rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
    >
      Preparar alteração
    </button>

    <p aria-live="polite" className="text-sm font-medium text-emerald-700">
      {mensagem}
    </p>
  </form>
);

Atualização imutável do estado

Observe:

setFormulario((estadoAtual) => ({
  ...estadoAtual,
  nome: event.target.value,
}));

O processo é:

  1. receber o estado atual;
  2. criar um novo objeto;
  3. copiar todas as propriedades;
  4. substituir apenas nome;
  5. entregar o novo objeto ao React.

Não faça:

formulario.nome = event.target.value;

Isso altera o objeto diretamente e não segue o padrão imutável esperado para o estado.

Conversão dos campos numéricos

O valor de um input, mesmo com type="number", chega como texto.

Number(event.target.value)

Converte o texto antes de armazená-lo em preco ou estoque.

Tipo obtido pela propriedade

Produto["categoria"]

Significa:

use exatamente o tipo da propriedade categoria da interface Produto.

Assim, não precisamos repetir manualmente a união.

src/routes/Produtos/index.tsx

import { useState } from "react";
import { useNavigate } from "react-router";

import CardProduto from "../../components/CardProduto";
import { listaProdutos } from "../../data/produtos";
import type { FiltroCategoria } from "../../types/produto";

export default function Produtos() {
  const navigate = useNavigate();

  const [busca, setBusca] = useState("");
  const [categoria, setCategoria] =
    useState<FiltroCategoria>("todos");

  const termoNormalizado = busca.trim().toLowerCase();

  const produtosFiltrados = listaProdutos.filter((produto) => {
    const nomeNormalizado = produto.nome.toLowerCase();
    const descricaoNormalizada = produto.descricao.toLowerCase();

    const correspondeAoTexto =
      nomeNormalizado.includes(termoNormalizado) ||
      descricaoNormalizada.includes(termoNormalizado);

    const correspondeACategoria =
      categoria === "todos" || produto.categoria === categoria;

    return correspondeAoTexto && correspondeACategoria;
  });

  function abrirEdicao(id: number) {
    navigate(`/editar-produtos/${id}`);
  }

  return (
    <main className="mx-auto max-w-7xl px-4 py-10">
      <header>
        <h1 className="text-3xl font-bold text-slate-900">
          Produtos
        </h1>
        <p className="mt-2 text-slate-600">
          Consulte e selecione um produto para editar.
        </p>
      </header>

      <section
        aria-label="Filtros dos produtos"
        className="mt-8 grid gap-4 rounded-2xl bg-slate-100 p-5 md:grid-cols-2"
      >
        <div>
          <label htmlFor="busca" className="mb-2 block font-medium text-slate-800">
            Buscar por nome ou descrição
          </label>
          <input
            id="busca"
            type="search"
            value={busca}
            onChange={(event) => setBusca(event.target.value)}
            className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3"
            placeholder="Exemplo: notebook"
          />
        </div>

        <div>
          <label htmlFor="categoria" className="mb-2 block font-medium text-slate-800">
            Categoria
          </label>
          <select
            id="categoria"
            value={categoria}
            onChange={(event) =>
              setCategoria(event.target.value as FiltroCategoria)
            }
            className="w-full rounded-xl border border-slate-300 bg-white px-4 py-3"
          >
            <option value="todos">Todos</option>
            <option value="notebook">Notebooks</option>
            <option value="smartphone">Smartphones</option>
            <option value="acessorio">Acessórios</option>
          </select>
        </div>
      </section>

      <p className="mt-5 text-sm text-slate-600" aria-live="polite">
        {produtosFiltrados.length} produto(s) encontrado(s).
      </p>

      {produtosFiltrados.length > 0 ? (
        <div className="mt-6 grid gap-6 md:grid-cols-2 xl:grid-cols-3">
          {produtosFiltrados.map((produto) => (
            <CardProduto
              key={produto.id}
              produto={produto}
              onEditar={abrirEdicao}
            />
          ))}
        </div>
      ) : (
        <p className="mt-8 rounded-2xl bg-amber-50 p-5 text-amber-900">
          Nenhum produto corresponde aos filtros escolhidos.
        </p>
      )}
    </main>
  );
}

src/routes/EditarProdutos/index.tsx

import { useState, type FormEvent } from "react";
import { Link, useParams } from "react-router";

import { listaProdutos } from "../../data/produtos";
import type {
  FormularioProduto,
  Produto,
} from "../../types/produto";

export default function EditarProdutos() {
  const { id } = useParams();
  const produtoId = Number(id);

  const produtoEncontrado = listaProdutos.find(
    (produto) => produto.id === produtoId,
  );

  if (!produtoEncontrado) {
    return (
      <main className="mx-auto max-w-4xl px-4 py-10">
        <h1 className="text-3xl font-bold text-slate-900">
          Produto não encontrado
        </h1>
        <p className="mt-3 text-slate-600">
          Não existe um produto com o identificador informado.
        </p>
        <Link
          to="/produtos"
          className="mt-6 inline-block font-semibold text-blue-700 underline"
        >
          Voltar para produtos
        </Link>
      </main>
    );
  }

  return (
    <main className="mx-auto max-w-4xl px-4 py-10">
      <header>
        <h1 className="text-3xl font-bold text-slate-900">
          Editar produto
        </h1>
        <p className="mt-2 text-slate-600">
          Produto selecionado: {produtoEncontrado.nome}
        </p>
      </header>

      <FormularioEdicao produto={produtoEncontrado} />
    </main>
  );
}

interface FormularioEdicaoProps {
  produto: Produto;
}

function FormularioEdicao({ produto }: FormularioEdicaoProps) {
  const [formulario, setFormulario] = useState<FormularioProduto>(() => ({
    nome: produto.nome,
    preco: produto.preco,
    categoria: produto.categoria,
    status: produto.status,
    estoque: produto.estoque,
    imagem: produto.imagem,
    descricao: produto.descricao,
  }));

  const [mensagem, setMensagem] = useState("");

  function salvarEdicao(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const produtoAtualizado: Produto = {
      id: produto.id,
      ...formulario,
    };

    console.table(produtoAtualizado);
    setMensagem("Alterações preparadas apenas na memória do navegador.");
  }

  return (
    <form
      onSubmit={salvarEdicao}
      className="mt-8 space-y-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
    >
      <div>
        <label htmlFor="nome" className="mb-2 block font-medium text-slate-800">
          Nome
        </label>
        <input
          id="nome"
          type="text"
          value={formulario.nome}
          onChange={(event) =>
            setFormulario((atual) => ({
              ...atual,
              nome: event.target.value,
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
          required
        />
      </div>

      <div className="grid gap-6 md:grid-cols-2">
        <div>
          <label htmlFor="preco" className="mb-2 block font-medium text-slate-800">
            Preço
          </label>
          <input
            id="preco"
            type="number"
            min="0"
            step="0.01"
            value={formulario.preco}
            onChange={(event) =>
              setFormulario((atual) => ({
                ...atual,
                preco: Number(event.target.value),
              }))
            }
            className="w-full rounded-xl border border-slate-300 px-4 py-3"
            required
          />
        </div>

        <div>
          <label htmlFor="estoque" className="mb-2 block font-medium text-slate-800">
            Estoque
          </label>
          <input
            id="estoque"
            type="number"
            min="0"
            step="1"
            value={formulario.estoque}
            onChange={(event) =>
              setFormulario((atual) => ({
                ...atual,
                estoque: Number(event.target.value),
              }))
            }
            className="w-full rounded-xl border border-slate-300 px-4 py-3"
            required
          />
        </div>
      </div>

      <div className="grid gap-6 md:grid-cols-2">
        <div>
          <label htmlFor="categoria-edicao" className="mb-2 block font-medium text-slate-800">
            Categoria
          </label>
          <select
            id="categoria-edicao"
            value={formulario.categoria}
            onChange={(event) =>
              setFormulario((atual) => ({
                ...atual,
                categoria: event.target.value as Produto["categoria"],
              }))
            }
            className="w-full rounded-xl border border-slate-300 px-4 py-3"
          >
            <option value="notebook">Notebook</option>
            <option value="smartphone">Smartphone</option>
            <option value="acessorio">Acessório</option>
          </select>
        </div>

        <div>
          <label htmlFor="status" className="mb-2 block font-medium text-slate-800">
            Status
          </label>
          <select
            id="status"
            value={formulario.status}
            onChange={(event) =>
              setFormulario((atual) => ({
                ...atual,
                status: event.target.value as Produto["status"],
              }))
            }
            className="w-full rounded-xl border border-slate-300 px-4 py-3"
          >
            <option value="disponivel">Disponível</option>
            <option value="baixo-estoque">Baixo estoque</option>
            <option value="indisponivel">Indisponível</option>
          </select>
        </div>
      </div>

      <div>
        <label htmlFor="imagem" className="mb-2 block font-medium text-slate-800">
          Endereço da imagem
        </label>
        <input
          id="imagem"
          type="url"
          value={formulario.imagem}
          onChange={(event) =>
            setFormulario((atual) => ({
              ...atual,
              imagem: event.target.value,
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
          required
        />
      </div>

      <div>
        <label htmlFor="descricao" className="mb-2 block font-medium text-slate-800">
          Descrição
        </label>
        <textarea
          id="descricao"
          rows={5}
          value={formulario.descricao}
          onChange={(event) =>
            setFormulario((atual) => ({
              ...atual,
              descricao: event.target.value,
            }))
          }
          className="w-full rounded-xl border border-slate-300 px-4 py-3"
          required
        />
      </div>

      <figure className="overflow-hidden rounded-2xl bg-slate-100 p-4">
        <img
          src={formulario.imagem}
          alt={`Prévia de ${formulario.nome}`}
          className="mx-auto max-h-72 rounded-xl object-contain"
        />
        <figcaption className="mt-2 text-center text-sm text-slate-600">
          Prévia da imagem informada
        </figcaption>
      </figure>

      <button
        type="submit"
        className="rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white hover:bg-slate-700"
      >
        Preparar alteração
      </button>

      <p aria-live="polite" className="text-sm font-medium text-emerald-700">
        {mensagem}
      </p>
    </form>
  );
}

useEffect é útil para sincronizar o componente com algo externo ao processo normal de renderização.

Neste codelab:

Portanto, isto é suficiente:

import { listaProdutos } from "../../data/produtos";

E isto seria trabalho desnecessário:

const [produtos, setProdutos] = useState<Produto[]>([]);

useEffect(() => {
  setProdutos(listaProdutos);
}, []);

O segundo exemplo cria:

  1. um estado inicialmente vazio;
  2. uma primeira renderização sem dados;
  3. um efeito apenas para copiar uma constante;
  4. uma segunda renderização;
  5. duas fontes representando a mesma informação.

Regra prática

Se o valor já pode ser importado ou calculado durante a renderização, não crie um efeito apenas para copiá-lo.

Hooks realmente usados

Hook

Página

Responsabilidade

useState

Produtos

Guardar busca e categoria escolhida.

useNavigate

Produtos

Abrir a página de edição com o id.

useParams

EditarProdutos

Ler o id do segmento dinâmico da URL.

useState

FormularioEdicao

Guardar os valores alterados e a mensagem.

Ao pressionar "Preparar alteração", criamos:

const produtoAtualizado: Produto = {
  id: produto.id,
  ...formulario,
};

Esse objeto existe apenas durante a execução atual da aplicação.

O que funciona

O que não acontece

Por que esse exercício continua importante?

Ele ensina o fluxo que também será necessário quando uma fonte externa for conectada:

  1. definir tipos;
  2. receber ou importar uma lista;
  3. renderizar com map;
  4. filtrar com filter;
  5. transportar o id;
  6. localizar com find;
  7. inicializar o formulário;
  8. montar o objeto atualizado.

Depois, a persistência será uma nova responsabilidade, sem apagar o aprendizado desta etapa.

Erro: módulo não encontrado

Cannot find module '../../data/produtos'

Confira:

Erro: propriedade não existe

Property 'preço' does not exist on type 'Produto'

A interface usa preco, sem acento. O nome precisa ser idêntico.

Erro: string não pode ser number

Type 'string' is not assignable to type 'number'

Converta valores numéricos vindos de campos:

Number(event.target.value)

Cards não aparecem

Confira se o map devolve JSX e se está usando produtosFiltrados.

produtosFiltrados.map((produto) => (
  <CardProduto key={produto.id} produto={produto} onEditar={abrirEdicao} />
))

Sempre vai para Error

Verifique:

  1. se a URL segue /editar-produtos/ID;
  2. se o valor foi convertido com Number;
  3. se existe produto com aquele id;
  4. se a comparação usa ===;
  5. se find recebe a lista correta.

O filtro não encontra maiúsculas

Normalize tanto o campo quanto os dados:

produto.nome.toLowerCase().includes(busca.trim().toLowerCase())

Hook chamado em lugar incorreto

Hooks devem ficar:

Exercício 1 — leitura de tipos

Explique com suas palavras:

  1. por que CategoriaProduto é uma união;
  2. por que preco é number;
  3. por que FormularioProduto não possui id;
  4. o que significa Produto[].

Exercício 2 — map

Crie um array contendo apenas os preços:

const precos = // complete

Depois, crie uma lista JSX contendo somente os nomes.

Exercício 3 — filter

Crie uma lista contendo produtos com estoque menor ou igual a 5.

const estoqueBaixo = // complete

Exercício 4 — find

Localize o produto com id 6.

Depois, tente localizar o id 100 e explique o resultado.

Exercício 5 — novo filtro

Adicione um checkbox:

Mostrar apenas produtos disponíveis

Use outro useState booleano e integre a condição ao filter existente.

Exercício 6 — leitura da URL

Explique o resultado de:

/editar-produtos/3
/editar-produtos/999
/editar-produtos/abc
/editar-produtos

Exercício 7 — apresentação final

Na página EditarProdutos, adicione antes do formulário:

Use uma lista de descrição com dl, dt e dd.

Exercício 1

  1. A união limita a categoria às opções conhecidas.
  2. O preço é numérico para permitir cálculo e formatação.
  3. O formulário edita dados do produto sem trocar sua identidade.
  4. Produto[] é um array formado por objetos que obedecem à interface.

Exercício 2

const precos = listaProdutos.map((produto) => produto.preco);
<ul>
  {listaProdutos.map((produto) => (
    <li key={produto.id}>{produto.nome}</li>
  ))}
</ul>

Exercício 3

const estoqueBaixo = listaProdutos.filter(
  (produto) => produto.estoque <= 5,
);

Exercício 4

const produtoSeis = listaProdutos.find(
  (produto) => produto.id === 6,
);

const produtoInexistente = listaProdutos.find(
  (produto) => produto.id === 100,
);

produtoSeis recebe o objeto encontrado. produtoInexistente recebe undefined.

Exercício 5

Estado:

const [somenteDisponiveis, setSomenteDisponiveis] = useState(false);

Controle:

<label className="flex items-center gap-2">
  <input
    type="checkbox"
    checked={somenteDisponiveis}
    onChange={(event) => setSomenteDisponiveis(event.target.checked)}
  />
  Mostrar apenas produtos disponíveis
</label>

Condição no filter:

const correspondeAoStatus =
  !somenteDisponiveis || produto.status === "disponivel";

return (
  correspondeAoTexto &&
  correspondeACategoria &&
  correspondeAoStatus
);

Exercício 6

URL

Resultado esperado

/editar-produtos/3

Encontra e apresenta o produto 3.

/editar-produtos/999

A rota corresponde, mas o produto não existe; mostra a mensagem local.

/editar-produtos/abc

A rota corresponde, mas a conversão não gera um id válido; mostra a mensagem local.

/editar-produtos

Não corresponde à rota com :id; o errorElement apresenta Error.

Exercício 7

<dl className="mt-6 grid gap-4 rounded-2xl bg-slate-100 p-5 sm:grid-cols-2">
  <div>
    <dt className="text-sm text-slate-500">Identificador</dt>
    <dd className="font-semibold text-slate-900">{produtoEncontrado.id}</dd>
  </div>
  <div>
    <dt className="text-sm text-slate-500">Categoria</dt>
    <dd className="font-semibold text-slate-900">{produtoEncontrado.categoria}</dd>
  </div>
  <div>
    <dt className="text-sm text-slate-500">Estoque</dt>
    <dd className="font-semibold text-slate-900">{produtoEncontrado.estoque}</dd>
  </div>
  <div>
    <dt className="text-sm text-slate-500">Status</dt>
    <dd className="font-semibold text-slate-900">{produtoEncontrado.status}</dd>
  </div>
</dl>

Checklist técnico

Resumo do fluxo completo

1. Produto define o contrato.
2. produtos.ts cria Produto[].
3. routes/Produtos/index.tsx importa o array.
4. useState guarda os filtros.
5. filter seleciona os produtos.
6. map cria os cards.
7. CardProduto informa o id escolhido.
8. useNavigate abre /editar-produtos/ID.
9. useParams lê o id definido por :id.
10. Number converte texto em número.
11. find localiza o objeto.
12. useState inicializa o formulário.
13. o formulário monta um novo Produto em memória.

Referências oficiais

Você concluiu o processo completo de consumo de um arquivo TypeScript local, desde a definição do tipo até a apresentação dos dados na página de edição.