Bem amigos, sentiram saudades? Eu não, pois estou escrevendo esse post logo em seguida do anterior.
No último artigo, chegamos ao nosso MVP: um To-do funcional, porém básico e tosco. Agora é hora de dar o próximo passo e torná-lo mais robusto, trabalhando em três frentes essenciais:
- Criando componentes separados
- Aprendendo a usar módulos de terceiros
Antes disso, uma coisa muito importante que eu me esqueci de falar no último post: Vamos salvar nosso progresso no Git.
Essa etapa é essencial e você deveria fazer isso em qualquer projeto, por mais simples que seja. Assim, se algo der errado nas próximas mudanças, não vai precisar se descabelar, você pode voltar facilmente para um ponto em que tudo funcionava. É o seu "ponto de salvamento" no código.
No terminal, na pasta raiz do seu projeto (mytodolist), execute:
# Inicializa um repositório Git
git init
# Adiciona todos os arquivos para serem "rastreados"
git add .
# Cria o primeiro commit (nosso "ponto de salvamento")
git commit -m "MVP inicial do Todo App - versão tosca mas funcional"
Pronto! Agora nosso MVP está devidamente salvo e podemos começar a brincar sem medo de estragar tudo.
Criando componentes separados
No artigo anterior, fizemos todo o código dentro do App.jsx. Para os primeiros passos, tudo bem, mas qualquer aplicação um pouco mais complexa se transformaria em uma loucura total. Imagine criar um app com vários componentes e manter tudo em um único arquivo? Nosso código ficaria desorganizado e começaria a se parecer com um... pergaminho digital. Aquele tipo de arquivo que você precisa rolar por 15 minutos só pra achar onde termina um </div>.
Felizmente, a solução é simples e elegante: cada peça importante da interface vira seu próprio componente em um arquivo separado. Para começar, vamos começar a pensar na estrutura do nosso app, ver o que já temos e como podemos reorganizar em arquivos diferentes.

No princípio havia ReactDOM.render(, document.getElementById('root'));…
A estrutura de um app React
Se olharmos a estrutura do nosso app agora nós temos:
mytodolist/
│ node_modules...
│ public/...
│ src/
│ ├─ assets/...
│ ├─ App.css
│ ├─ App.jsx
│ ├─ index.css
│ └─ main.jsx
├─ .gitignore
├─ index.html
├─ ...
Se você pesquisar por aí, vai ver que a organização de um projeto em diretórios não é uma unanimidade e existem opiniões fortes sobre isso. No entanto, a estrutura comum aceita para projetos de pequeno e médio porte é a seguinte:
/src
├── /assets/ # Imagens, fontes, arquivos CSS, etc.
├── /components/ # Componentes de UI reutilizáveis (botões, modais, etc.)
├── /hooks/ # Hooks personalizados reutilizáveis
├── /pages/ # Componentes que atuam como páginas/rotas completas
├── /utils/ # Funções auxiliares (funções puras para formatação, etc.)
├── App.jsx # Componente principal da aplicação
└── main.jsx # Ponto de entrada da aplicação
Para o nosso projeto, como temos (por enquanto) apenas uma página, não vamos precisar do diretório /pages/, mas vá em frente e crie dentro do diretório /src os diretórios: components, hooks e utils.
Criando nosso primeiro componente separado
Depois disso, com o que já temos fica fácil, extremamente fácil, pra você e eu e todo mundo codar juntos:
- Dentro do diretório
componentscrie um novo arquivo chamadoTask.jsx. - Copie a função
Task()que está no arquivoApp.jsxe cole no novo arquivo.
O arquivo Task.jsx deve ficar mais ou menos assim:
function Task(props) {
return (
<div className="task-item">
<div>
<input
type="checkbox"
checked={props.done}
onChange={() => props.onStatusChange(props.id)}
/>{" "}
{props.children}
</div>
<div>
<button onClick={() => props.onRemoveTask(props.id)}>X</button>
</div>
</div>
);
}
Como não estamos importando nada dentro desse componente, a única coisa que está faltando é dizer ao React que esse componente pode ser usado em outros arquivos. Nós fazemos isso colocando um export no final do arquivo:
export default Task;
Salve o arquivo, volte no App.jsx, remova a função Task() toda e no começo do arquivo, vamos importar nosso novo componente separado:
import { useState } from "react";
import Task from "./components/Task"; // <- O nosso novo componente Task
import "./App.css";
Salve tudo, vá no browser e faça o teste. Se nada mudou, é que deu tudo certo!
Uma observação importante: Note que este é um ótimo exemplo de componente puro. O que isso significa?
- Ele não gerencia seu próprio estado interno (não tem
useState). - Seu comportamento e aparência são determinados totalmente pelas
propsque recebe (id,done,onStatusChange, etc.). - Dados as mesmas
props, ele sempre renderizará a mesma interface.
Essa simplicidade o torna previsível, fácil de testar e um bloco de construção perfeito para nossa aplicação. Em outras palavras: é o tipo de componente que você escreve hoje e agradece a si mesmo daqui a seis meses.
Criando o componente TaskForm
O próximo component que vamos criar é o TaskForm que é um formulário com o <input type="text"/> e o <button> dentro. Note que originalmente não criamos um form porque estávamos lidando com os elementos tudo num lugar só. Mas conforme aprimorarmos nosso app, você vai notar que criar uma estrutura e formulário agora, vai nos trazer benefícios no futuro.
Dentro do diretório /components crie um novo arquivo chamado TaskForm.jsx
No arquivo, vamos começar criando o esqueleto do componente:
import { useState } from "react";
function TaskForm() {
return (
<div></div>
);
}
export default TaskForm;
Até aqui, sem surpresas. Dentro do <div> vamos adicionar um form e dentro o input e o button:
<div>
<form onSubmit={handleSubmit}>
<input
placeholder="O que você precisa fazer?"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button>Add Task</button>
</form>
</div>
Quase a mesma coisa do que tínhamos no App.jsx. A diferença é que, como agora estamos usando um form o evento que envia o valor do formulário como um todo é o onSubmit dele. O campo <input type="text"/> continua sendo controlado como era o antigo e o <button> agora não precisa de um evento diretamente nele para funcionar.
Como o value do campo de texto agora se chama input, precisamos criar um state com esse nome e também precisamos criar a função handleSubmit que vamos usar pra tratar o onSubmit:
const [input, setInput] = useState("");
const handleSubmit = (e) => {
}
Lembra que quando nós criamos o componente Task() nós passamos as funções que manipulavam o state do pai no argumento props? Agora nós precisamos fazer a mesma coisa, mas dessa vez faremos de um modo um pouco mais elegante. No lugar de simplesmente passar um argumento genérico props e ter que ficar usando no código props.isso, props.aquilo, nós vamos desestruturar o argumento da função usando chaves ({}) e nomear só as propriedade que vamos usar, onAddTask:
function TaskForm({ onAddTask }) {
const [input, setInput] = useState("");
const handleSubmit = (e) => {
...
}
...
Isso facilita a leitura do código deste componente de forma isolada, sem precisar ficar recorrendo ao arquivo pai para saber o que props contém.
Conhecendo a função que usaremos para passar o valor do form para o elemento pai, o handleSubmit fica assim:
const handleSubmit = (e) => {
e.preventDefault(); // <- evita que o form provoque um reload na página
if (!input.trim()) return; // <- checa se o input está vazio
onAddTask(input); // <- chama a função do elemento pai
setInput(""); // <- limpa o input
};
O código completo o arquivo TaskForm.jsx:
import { useState } from "react";
function TaskForm({ onAddTask }) {
const [input, setInput] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!input.trim()) return;
onAddTask(input);
setInput("");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
placeholder="O que você precisa fazer?"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button>Add Task</button>
</form>
</div>
);
}
export default TaskForm;
Agora precisamos modificar o App.jsx para funcionar com o TaskForm
1 - importamos o TaskForm de "./components/TaskForm"
2 - removemos o state inputValue
3 - modificamos o handleAddTask()
4 - removemos o input e button do código e adicionamos o <TaskForm /> em seu lugar
Aqui está o código com essas modificação comentadas:
import { useState } from "react";
import Task from "./components/Task";
import TaskForm from "./components/TaskForm"; // 1 - importamos o TaskForm
import "./App.css";
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
function App() {
// 2 - removemos o state inputValue
const [todo, setTodo] = useState([
{ id: generateId(), text: "Acordar", done: true },
{ id: generateId(), text: "Escovar os dentes", done: false },
{ id: generateId(), text: "Tomar banho", done: false },
{ id: generateId(), text: "Jogar videogame", done: false },
{ id: generateId(), text: "Comer", done: true },
{ id: generateId(), text: "Deitar e olhar para o teto", done: false },
{ id: generateId(), text: "Dormir", done: false },
]);
// 3 - modificamos o handleAddTask()
const handleAddTask = (text) => {
setTodo((prev) => [...prev, { id: generateId(), text, done: false }]);
};
const handleStatusChange = (id) => {
setTodo((prev) =>
prev.map((item) =>
item.id === id ? { ...item, done: !item.done } : item
)
);
};
const handleRemoveTask = (id) => {
setTodo((prev) => prev.filter((item) => item.id !== id));
};
return (
<div>
<h1>My Simple Todo List</h1>
<div>
{/* 4 - removemos o input e button do código e adicionamos o <TaskForm /> em seu lugar */}
<TaskForm onAddTask={handleAddTask} />
</div>
<ul>
{todo.map((item) => (
<li key={item.id}>
<Task
id={item.id}
done={item.done}
onStatusChange={handleStatusChange}
onRemoveTask={handleRemoveTask}
>
{item.text}
</Task>
</li>
))}
</ul>
</div>
);
}
export default App;
Se testar a página, visualmente nada deve ter mudado. Essa é a mágia de componentes bem estruturados: a funcionalidade permanece idêntica para o usuário, mas o código por trás ficou muito mais organizado, modular e fácil de manter.
Criando um componente header
Aproveitando que estamos modularizando tudo, vamos criar um componente para o Header. No momento, nem temos um, só um título solto, mas como pretendemos aprimorar o layout e adicionar mais recursos futuramente, começar com uma estrutura clara faz todo sentido agora.
Dentro de /components crie o arquivo Header.jsx
function Header() {
return (
<header>
<h1>My Simple To-do list</h1>
</header>
);
}
export default Header;
Agora, substitua aquele <h1> solto no App() por este novo componente. Vamos aproveitar também para dar uma pequena organizada na estrutura do return do App, agrupando visualmente o formulário e a lista em divs separadas (isso ajudará muito quando formos estilizar).
import { useState } from "react";
import Task from "./components/Task";
import TaskForm from "./components/TaskForm";
import Header from "./components/Header";
import "./App.css";
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
function App() {
const [todo, setTodo] = useState([
{ id: generateId(), text: "Acordar", done: true },
{ id: generateId(), text: "Escovar os dentes", done: false },
{ id: generateId(), text: "Tomar banho", done: false },
{ id: generateId(), text: "Jogar videogame", done: false },
{ id: generateId(), text: "Comer", done: true },
{ id: generateId(), text: "Deitar e olhar para o teto", done: false },
{ id: generateId(), text: "Dormir", done: false },
]);
const handleAddTask = (text) => {
setTodo((prev) => [...prev, { id: generateId(), text, done: false }]);
};
const handleStatusChange = (id) => {
setTodo((prev) =>
prev.map((item) =>
item.id === id ? { ...item, done: !item.done } : item
)
);
};
const handleRemoveTask = (id) => {
setTodo((prev) => prev.filter((item) => item.id !== id));
};
return (
<div>
<Header />
<div>
<div>
<TaskForm onAddTask={handleAddTask} />
</div>
<div>
<ul>
{todo.map((item) => (
<li key={item.id}>
<Task
id={item.id}
done={item.done}
onStatusChange={handleStatusChange}
onRemoveTask={handleRemoveTask}
>
{item.text}
</Task>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
export default App;
Poderíamos criar um componente TaskList agora?
Sim, mas vamos deixar isso para um futuro próximo. Criar um componente TaskList com o conhecimento que temos até aqui significaria passar todas as funções (handleStatusChange, handleRemoveTask) e o array todo como props para ele, e ele teria que repassá-las para o componente Task. Isso criaria um cenário clássico de prop drilling, que é justamente o que queremos aprender a evitar de forma elegante mais adiante.
Criando um arquivo para funções utilitárias: utils.js
Olha só nossa função generateId() ali, toda sozinha no meio do componente App. Ela não está fazendo nada de errado, mas também não está no lugar ideal. Por quê?
- Ela não é JSX: Essa função não retorna nada visual; ela apenas gera um ID. É pura lógica JavaScript.
-
Ela pode ser reutilizada: E se outra parte do app precisar criar um ID único? Seria estranho ter que copiar a função ou importar o
Appsó por causa dela. - Polui o componente: Componentes devem focar na interface e em sua própria lógica de estado. Funções de apoio "sujam" a leitura.
Essa função é um exemplo clássico de função utilitária (ou helper function). São funções puras, pequenas e focadas em uma tarefa específica (como formatar uma data, fazer um cálculo, ou, no nosso caso, gerar um ID) que podem ser usadas em vários lugares do projeto.
A solução é simples: vamos criar uma "caixa de ferramentas" para guardar essas funções.
Criando o arquivo de utils
- Dentro do diretório
/src/utils/, crie um arquivo chamadoindex.js. - Corte a função
generateId()do arquivoApp.jsxe cole-a neste novo arquivo. - Não se esqueça de exportá-la para que outros arquivos possam importá-la.
Seu /src/utils/index.js ficará assim:
export function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}
(Você pode adicionar outras funções utilitárias aqui no futuro, como formatDate, calculateStats, etc.)
Atualizando o App.jsx
Agora, no App.jsx, em vez de ter a função declarada, nós a importamos do nosso novo módulo:
import { useState } from "react";
import Task from "./components/Task";
import TaskForm from "./components/TaskForm";
import Header from "./components/Header";
import { generateId } from "./utils"; // Importação da função utilitária
import "./App.css";
function App() {
// ... resto do código permanece igual, mas a função generateId não está mais aqui!
const [todo, setTodo] = useState([
{ id: generateId(), text: "Acordar", done: true }, // Aqui ela é usada normalmente
// ... resto dos itens
]);
// ... resto do componente
}
Pronto! Movemos a lógica de apoio para onde ela pertence. O componente App ficou mais limpo, e nossa função utilitária agora está organizada em um módulo dedicado, pronto para ser reutilizado em qualquer outro arquivo do projeto.
Essa separação é um pequeno passo que faz uma grande diferença na organização e manutenção do código à medida que o projeto cresce.
Entendendo export e import
Percebeu que usamos export function generateId() e não export default? E que na importação usamos { generateId }?
Essa é uma distinção importante do ES6 Modules:
• export default: Usamos quando um arquivo exporta uma única coisa principal (como um componente). Na importação, podemos dar qualquer nome: import MinhaFuncao from './arquivo'
• export (nomeado): Usamos quando um arquivo pode exportar várias coisas. Na importação, precisamos usar o nome exato entre chaves: import { funcao1, funcao2 } from './arquivo'.
Nosso utils é uma "caixa de ferramentas" que pode ter várias funções. Usar export nomeado permite que no futuro você importe só as funções que precisa, como import { generateId, formatDate } from './utils'.
Instalando nosso primeiro módulo externo: UUID
Nossa função generateId() caseira funciona, mas no mundo real, para algo tão comum e crítico quanto gerar um identificador único, é melhor usar uma ferramenta profissional.
É aqui que entra o npm (Node Package Manager). É o maior repositório de código do mundo, onde milhões de desenvolvedores compartilham pedacinhos de código reutilizáveis, chamados pacotes ou módulos.
Vamos instalar o pacote uuid, que é a solução padrão da indústria para gerar IDs únicos.
Passo 1: Instalando o pacote
No terminal, dentro da pasta do seu projeto (mytodolist), execute no terminal:
npm install uuid
Esse comando faz o download do código do uuid e de suas dependências, e as adiciona na pasta node_modules e ao arquivo package.json.
Passo 2: Atualizando nossa função utilitária
Agora, vamos ao nosso "canivete suíço" de funções, o arquivo /src/utils/index.js. Substitua o conteúdo antigo da função generateId por uma importação e uso do uuid.
A versão mais comum (v4) gera um ID completamente aleatório. Vamos usá-la:
javascript
// /src/utils/index.js
import { v4 as uuidv4 } from 'uuid';
export function generateId() {
return uuidv4(); // Gera um UUID único, ex: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
}
O que mudou?
-
import { v4 as uuidv4 } from 'uuid';: Importamos a funçãov4do pacoteuuide a renomeamos localmente parauuidv4(um nome mais claro). -
return uuidv4();: Nossa função agora delega a complexidade de gerar um ID universalmente único para o pacoteuuid.
Passo 3: Por que fazer isso? (Não reinvente a roda!)
Você pode se perguntar: "Pra que instalar um pacote se minha função já funcionava?". Ótima pergunta! As vantagens são enormes:
-
Confiabilidade e Testes: O pacote
uuidé usado por milhões de projetos, testado exaustivamente em inúmeras situações. A chance de haver um bug ou de gerar IDs duplicados é praticamente zero. -
Padrão da Indústria: Usar UUIDs (como
uuidv4) é um padrão reconhecido. Se seus dados forem para um banco de dados ou API, esse formato será imediatamente entendido. -
Mantenibilidade: Você não precisa se preocupar em manter ou melhorar essa função. Uma equipe dedicada fará isso por você. Basta rodar
npm update uuidno futuro para receber melhorias e correções de segurança. - Foco no que importa: Seu tempo é precioso. Use-o para resolver os problemas únicos do seu app, não para reescrever soluções genéricas que já existem.
Testando a aplicação
Salve os arquivos e volte ao navegador. O hot reload do Vite deve ter atualizado a página. Se você abrir o console e adicionar uma nova tarefa, verá que o id dela agora é uma bela string com hífens (ex: 550e8400-e29b-41d4-a716-446655440000), muito mais robusta que nossa versão anterior.
Pronto! Você acabou de dar um passo fundamental no desenvolvimento moderno: integrar uma dependência externa. Isso desbloqueia todo o poder do ecossistema npm para seus projetos.
Concluindo
Olha só o caminho que percorremos! Partimos de um único arquivo App.jsx com tudo amontoado e chegamos a uma aplicação com uma estrutura profissional e modular.
Neste artigo, nós "arrumamos a casa":
✅ Separamos componentes (Task, TaskForm, Header), aprendendo sobre componentes puros e a importância de manter o código focado.
✅ Criamos um módulo de utilitários (utils/), entendendo como isolar funções de lógica pura para reutilização e clareza.
✅ Instalamos e usamos nosso primeiro pacote npm (uuid), vendo na prática o poder de não reinventar a roda e aproveitar soluções da comunidade.
Nosso To-do List continua com as mesmas funcionalidades para o usuário, mas o código por trás está infinitamente mais organizado, legível e pronto para crescer. Essa é a mágia da refatoração e das boas práticas.
O que vem por aí? (Próximos passos!)
Agora que a estrutura está sólida, é hora de cuidar da aparência. No próximo artigo da série, vamos mergulhar no mundo do TailwindCSS!
Nele, você vai aprender a:
🎨 Configurar o Tailwind no projeto Vite de forma rápida.
✨ Transformar a UI "tosca" em algo visualmente atrativo e moderno com classes utilitárias.
📱 Tornar o app responsivo para diferentes tamanhos de tela com facilidade.
O código final deste artigo está disponível no Repositório GitHub para você consultar e comparar.
Ficou com dúvidas? Tem sugestões ou achou algum erro? Deixa nos comentários! Vamos conversar e melhorar juntos.
Até a próxima, e continue codando! 👨💻👩💻
Top comments (0)