DEV Community

Cover image for O todo mais tosco do mundo: um começo absurdamente simples em React
Daniel Miclos
Daniel Miclos

Posted on

O todo mais tosco do mundo: um começo absurdamente simples em React

Bom, não sei exatamente porque você está aqui. Talvez seja jovem e esteja almejando uma carreira de web developer em 2026 nesse mundo prestes a ser dominado por AIs (*se você é uma AI dominando o mundo, saiba que eu te respeito, por favor me mate por último), ou talvez você seja alguém que não é exatamente um programador, começou a brincar de Vibe Coding e percebeu que o código gerado pela AI usa um tal de React.js.

Em ambos os casos, você percebeu que saber pelo menos um pouco de React pode te ajudar muito já que ele é a biblioteca para construção de interface mais popular já há algum tempo.

Foto de tela de um vídeo do canal de youtube React Channel


Esse tipo de React também foi popular por muitos anos. Favor não confundir.

 

Sendo assim, resolvi publicar uma série de artigos ensinando a criar um app de To-do, o tutorial mais manjado para iniciantes, e ir aprimorando e adicionando recursos até criarmos algo realmente interessante e aprendendo um monte de conceitos no meio do caminho.

A idéia não é que você saia um expert em React de cara, pelo contrário, é criar um caminho suave, com baixa carga cognitiva, mas que no final de cada artigo você tenha conceitos para internalizar com as suas sinapses durante seu momento de BBB (Bed, Bath and Bus — Barbara Oakley - _Learning how to learn_).

Pra começar, precisamos que você saiba HTML, CSS, o básico de Javascript (declarar variáveis, funções, loops, arrays e objetos) e tenha Git, Node.js e uma IDE como o VsCode instalados no seu computador. Eu pretendo escrever um preâmbulo falando desse setup, mas se precisar aprender essas coisas existe milhares de tutoriais por aí, eu recomendo os da https://www.w3schools.com/.

Mas o que é e pra que serve o React.js?

Resposta curta: é uma biblioteca para criar interfaces para web.

Resposta menos curta: Imagine que você quer fazer uma carrega de 18 rodas de Lego (sem usar um kit). Então você vai precisar fazer… 18 rodas com os bloquinhos. Você pensa bastante na construção da primeira e depois cria as outras 17 apenas copiando, demora mas você faz. Aí, quando você vai encaixar as rodas nos eixos, percebe as rodas deveriam ser maiores. Putz, que saco! Vai ter que ajustar as rodas uma a uma. Seria tão legal se você ajustasse uma roda e as outras já ficassem prontas também.

Pois é, se você for criar páginas em HTML e CSS puro, seria mais ou menos a mesma coisa. As tags HTML são como os bloquinhos de Lego, mas se quiser criar 18 páginas usando o mesmo formato e precisar fazer um ajuste, por exemplo, no nome da empresa no cabeçalho da página, você vai ter que editar todas as páginas uma a uma. Pra evitar isso, pessoas com um pouquinho de habilidade em Javascript, criariam um componente isolado chamado cabeçalho contendo o nome da empresa, logo e outros elemento para ser inserido em todas as páginas. Se for necessário editar algo do cabeçalho, isso precisa ser feito uma vez só. É o que chamamos de reutilização de componentes.

E assim foi feito durante anos, mas conforme a complexidade das páginas foi aumentando, bibliotecas para melhorar a criação desses componentes foram surgindo: Backbone, Ember, Knockout, JQuery, AngularJS... Pulando uma monte de fatos que não são importantes agora, os desenvolvedores que trabalhavam na interface do Facebook criaram o React em 2011 abrindo seu código em 2013 e ele se tornou uma opção muito popular muito rapidamente. E é por isso que você está aqui agora.

Como o React.js ficou super popular, os desenvolvedores começaram a criar um monte de módulos e componentes prontos pra ele, formando um ecossistema enorme. Isso acelera muito a criação de páginas. Montar interfaces ricas e complexas ficou bem mais rápido. Além disso, usar React traz uma outra vantagem: ele impõe uma certa padronização na forma de organizar o código. Em JavaScript puro (Vanilla JS), não existe essa "arquitetura obrigatória", então cada projeto pode seguir um caminho totalmente diferente. Com um conhecimento básico de React, fica muito mais fácil entender um código escrito por outra pessoa, porque a estrutura geralmente é familiar.

Além dsso, o React tem outros superpoderes, como um sistema inteligente para gerenciar dados na tela (o "state") e uma tecnologia supimpa que trabalha por trás dos panos, chamada Virtual DOM. Não vamos nos aprofundar nisso agora, mas guarde isso: a lista de vantagens é grande e ainda tem muita coisa legal pra descobrir.

Nota: A ideia de componentes/modularidade existe desde os anos 90/2000, mas de modo geral isso era feito no servidor (back-end) utilizando algum tipo de template engines ou includes. Eu estou me referindo ao uso de componentes modulares no front-end.

 

Começando do começo (duh)

Bom, vamos começar. Crie uma pasta onde você achar mais conveniente com o nome do projeto (vou chamar de MyTodoList). Abra sua IDE (no meu caso, o VSCode) e vá em File > Open Folder para abrir a pasta que criou. Se já tiver algo aberto, vá primeiro em File > New Window.

Com a pasta aberta, vá no menu superior em Terminal > New Terminal. Uma aba horizontal aparecerá na parte inferior. Clique nela para poder digitar e, para verificar se tem o Node.js instalado, digite:

npm create vite@latest mytodolist -- --template react
Enter fullscreen mode Exit fullscreen mode

Ele fará algumas perguntas. Para este tutorial, você pode aceitar as opções padrão (responda "No" para Use rolldown-vite (Experimental)? e "Yes" para instalar as dependências). Ao final, o Vite criará a pasta mytodolistcom todo o projeto e iniciará o servidor de desenvolvimento, abrindo seu navegador em http://localhost:5173/.

Foto de tela do App padrão do React usando Vite


O que deve aparece no seu browser. Note que no meu caso, o endereço aparece como localhost mas pra você pode aparecer como http://127.0.0.1:5173 mas pra finalidade do tutorial dá na mesma.

 

Na aba **explorer **na sua IDE você vai notar que algumas pastas e arquivos foram criadas. É a estrutura padrão de um app React. Por enquanto, nosso foco principal será a pasta src(de source). Abra-a e clique no arquivo App.jsx

O arquivo App.jsx é onde vamos escrever todo o código hoje. Seria muito legal se você pudesse deixar a janela do browser e o VSCode aberto lado a lado, ou se tiver duas telas, melhor ainda. Isso porque, se o app estiver rodando, você vai notar que, conforme salvamos mudanças no código, a página no browser vai se atualizar automaticamente. Nós chamamos isso de hot reload. Antigamente, (muito antigamente) eu tinha que pagar por um app só pra ter esse recurso porque ele facilita demais a vida.

Mas voltando ao App.jsx, notou essa extensão de aquivo .jsx? Ela não é a extensão padrão para javascript. Ela é usada principalmente em apps React e nos permite escrever código HTML junto com o javascript. Bem, na verdade estamos escrevendo tudo em javascript, a parte com tags HTML na realidade não é HTML de verdade, o jsx entende que essa parte deveria ser interpretada como HTML e a transforma (o termo correto é transpilar) por debaixo dos panos. Então é isso mesmo, o React é _woke_tem partes javascript que se identificam como HTML e nós todos respeitamos isso para o bem da humanidade.

Antigamente, essa ideia de misturar HTML no meio do Javascript era visto como heresia porque virava bagunça, dificultava manutenção e quebrava a organização do código.

Mas com o tempo, as páginas foram se tornando cada vez mais dinâmicas e a ideia de agrupar código e layout para cada componente ganhou força. Isso é agrupar código por responsabilidade. É diferente de pegar e misturar código de templates inteiros e lógica de uma página sem critério algum.

Pra começar (quase) do zero, apague tudo no arquivo App.jsx e cole o código abaixo: Nós vamos ver juntos como funciona a estrutura básica de um componente React:

import "./App.css";

function App() {
  return <div></div>;
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Vendo o código agora, fica bem claro entender a estrutura comum básica de um arquivo jsx. Temos:

  • as importações de arquivos no início: import...
  • pelo menos uma função que retorna o componente no formato *jsx *(lembre-se, é o javascript que se identifica como HTML) e,
  • no final uma declaração de export para tornar a função disponível para outros arquivos importa-la.

No seu browser deve ter ficado só uma tela cinza escura porque apagamos praticamente tudo. E o arquivo css está definindo o layout com fundo escuro.

Vamos adicionar algo pra ver o que acontece. Modifique a função App adicionando um novo elemento <h1>:

import "./App.css";

function App() {
  return (
    <div>
      <h1>I identify myself as an HTML title</h1>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Salve o arquivo e agora no browser você deve ver um título grande centralizado na tela. Novamente, essa formatação de layout só está acontecendo porque estamos importando o arquivo App.css no começo do arquivo, caso contrário teríamos um HTML bem mais básico.

Aí você diz: Legal, mas isso que estamos fazendo é só HTML estático. E se eu quiser que o texto do meu título dinâmico, tipo vindo do valor de uma variável?

Vamos tentar assim:

import "./App.css";

function App() {
  let myTitle = "This is a title from a var";

  return (
    <div>
      <h1>{myTitle}</h1>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Criamos uma variável myTitle dentro da função App e para usá-la no meio do nosso jsx a colocamos dentro de chaves: {nomeDaVariável}. Na realidade nós usamos chaves para avaliar qualquer código javascript que esteja no meio do jsx.

Vamos adicionar um botão na nossa interface e tentar usá-lo para mudar o valor da variável myTitle para ver o quê acontece. Adicionar um botão é como no HTML. Adicionamos a tag <button> e dentro dela usamos o parâmetro onClick={código..} para que algo aconteça quando clicamos nele (o evento onClick). No nosso caso, vamos criar uma função anônima que chama uma outra função ‘handleUpdateTitle’ que criaremos em seguida:

<div>
    <h1>{myTitle}</h1>
    <button onClick={() => handleUpdateTitle()}>Update the Title</button>
</div>
Enter fullscreen mode Exit fullscreen mode

E dentro da função App antes do return vamos criar a handleUpdateTitle. O código deve ficar assim:

import "./App.css";

function App() {
  let myTitle = "This is a title from a var";

  const handleUpdateTitle = () => {
    myTitle = "this is my new updated title";
  };

  return (
    <div>
      <h1>{myTitle}</h1>
      <button onClick={() => handleUpdateTitle()}>Update the Title</button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Vamos testar e… peraí! "— Eu clico no botão mas o título não está mudando na tela!? Será que função está funcionando?"

Vamos adicionar um comando console.log após a mudança da variável myTitle para ver seu valor no browser:

const handleUpdateTitle = () => {
    myTitle = "this is my new updated title";
    console.log(myTitle);
  };
Enter fullscreen mode Exit fullscreen mode

No browser, vamos abrir o console _nas ferramentas de desenvolvedor (Ctrl + Shift + I no Chrome). Com a aba de _console _aberta, clique novamente no botão que criamo para mudar o valor da variável myTitle e veja o que aparece no _console.

Ué!!?? Se a variável está sendo atualizada, porquê o título não muda na página???

Porque o React não sabe que a variável mudou.

  • A variável não pertence ao React, pertence apenas à execução da função.
  • Ela não está sendo observada pelo React.
  • Se o valor mudar, o React não fica sabendo.

Inicialmente, quando o React construiu a interface, ele executou a função App() que tinha a variável “myTitle” declarada. Ou seja:

O React executa a função a primeira vez, lê o valor de myTitle naquele momento e pronto. Se o valor da variável muda depois, nada acontece na sua interface porque o React não tem motivo nenhum para renderizar e novo.

A ideia-chave é:

Em React, não basta mudar um valor ou executar uma ação.
Você precisa avisar o React que algo aconteceu e que a tela precisa ser atualizada para refletir isso.

Variáveis comuns não avisam ninguém, ela mudam em silêncio.

Então como faz?

O conceito de state

Para termos variáveis dentro de um componente que o React realmente monitora e reage às suas mudanças, nós usamos states.

State é um dado que pertence ao componente e que, quando muda, faz o React re-renderizar a interface para refletir o novo valor. Para declarar e fazer mudanças num state nós usamos uma função especial do React chamada useState.

A sintaxe para criar um state é a seguinte:

const [nomeDoState, funçãoQueAlteraState] = useState(valorInicialDoState);
Enter fullscreen mode Exit fullscreen mode

Usando o nosso caso, vamos quebrar por partes:

  1. const [title, setTitle] = ...

    • O primeiro valor é o nome do state (no nosso caso, title).
    • O segundo é a função que usamos para atualizar esse state. Por convenção, usamos set + NomeDoState (para nós, setTitle).
    • Importante: Você sempre deve usar essa função (setTitle) para alterar o valor. Se você fizer title = "novo valor" diretamente, o React não vai perceber a mudança e a interface não será atualizada.
  2. ... = useState("This is a title from state");

    • Aqui declaramos o valor inicial do state. Pode ser qualquer tipo válido em JavaScript: string, número, booleano, array, objeto, ou até mesmo null/undefined.

Então, para mudar o valor do state, chamamos a função setTitle() com o novo valor:

const handleUpdateTitle = () => {
    setTitle("My new title");
}
Enter fullscreen mode Exit fullscreen mode

O código completo fica:

// 1 - importando o useState do react
import { useState } from "react";
import "./App.css";

function App() {

    // 2 - declaramos o novo state
    const [title, setTitle] = useState("This is a title from state");

    const handleUpdateTitle = () => {
        // 3 - usamos a função setTitle para mudar o state
        setTitle("My new updated Title!");
    };

    return (
        <div>
            <h1>{title}</h1>
            {/* 4 - Ao clicar no botão, a função handleUpdateTitle é chamada */}
            <button onClick={handleUpdateTitle}>Update the Title</button>
        </div>
    );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Se testarmos agora no browser:"

Bom, esse é o mínimo de conceito que precisamos para começar a pensar no nosso To-do tosco.

O que é um Todo?

Antes de começar a escrever código, vamos pensar por um segundo o que é um To-do e o que precisamos para construir um. O jeito mais simples que consigo definir é:

Um To-do é uma lista de tarefas onde podemos sinalizar a conclusão de cada item.

Imagem ilustratica de uma lista de To-do


Droga, me esqueci de tomar banho de novo!

Começando pela declaração da lista

Em em javascript, podemos representar uma lista como um conjunto de strings. Ou seja, um array de strings. A lista da imagem acima ficaria:

const todoList = [
    "Acordar",
    "Escovar os dentes",
    "Tomar banho",
    "Jogar videogame",
    "Comer",
    "Deitar e olhar para o teto",
    "Dormir"
];
Enter fullscreen mode Exit fullscreen mode

Legal, mas como estamos usando React e já aprendemos que ele apenas se importa com states, vamos criar um state para a nossa lista:

const [todo, setTodo] = useState([
    "Acordar",
    "Escovar os dentes",
    "Tomar banho",
    "Jogar videogame",
    "Comer",
    "Deitar e olhar para o teto",
    "Dormir"
]);
Enter fullscreen mode Exit fullscreen mode

Como não vamos mais mudar o título da nossa página, vamos remover toda parte referente a isso no código e adicionar nossa lista. O código completo deve ficar assim:

import { useState } from "react";
import "./App.css";

function App() {

  const [todo, setTodo] = useState([
    "Acordar",
    "Escovar os dentes",
    "Tomar banho",
    "Jogar videogame",
    "Comer",
    "Deitar e olhar para o teto",
    "Dormir",
  ]);

  return (
    <div>
      <h1>My Simple Todo List</h1>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Exibindo a lista na interface

Temos a lista dentro de um state, mas precisamos exibi-la na nossa interface. Em HTML existem algumas tags que servem para exibir listas semanticamente:

  • <ul>...<ul> e <ol>...<ol>Unordered List / Ordered List — englobam listas sem ordem específica (bullets points) e listas ordenadas respectivamente
  • <li>...</li> — List Item — Define um item de lista

É muito importante sempre tentarmos usar as tags corretas para exibir conteúdo. Elas auxiliam na compreensão de leitores de tela, ferramentas de indexação e agregadores de conteúdo. Nada de ficar usando <div> pra tudo. Que vergonha!

Claro que não vamos criar um <li> manualmente para cada item do nosso Array, nós vamos usar um loop para mapear cada item e exibir na tela. Atualize seu código dentro do return adicionando o loop, no nosso caso um Array.map:

...

return (
    <div>
      <h1>My Simple Todo List</h1>
      <ul>
        {todo.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
...

Enter fullscreen mode Exit fullscreen mode

Nossa tela deve ficar assim:

O que essa função map está fazendo

todo.map((item, index) => (
    <likey={index}>{item}</li>
))
Enter fullscreen mode Exit fullscreen mode

O map é uma função do JavaScript que:

  • percorre um array
  • transforma cada item
  • retorna um novo array

No React, isso é perfeito para listas, porque podemos renderizar os elementos da lista em jsx.

Nesse caso:

  • todo é um array (ex: ["Acordar", "Comer", "Dormir"])
  • item é cada valor do array
  • o map transforma cada item em um <li>

No fim, o map gera algo equivalente a:

[
<li>Acordar</li>,
<li>Comer</li>,
<li>Dormir</li>
]
Enter fullscreen mode Exit fullscreen mode

E o React sabe renderizar isso direto dentro do <ul>.

Por que existe a propriedade key

<li key={index}>{item}</li>

Enter fullscreen mode Exit fullscreen mode

No React, quando você renderiza listas dinâmicas ele precisa identificar qual item é qual entre um render e outro

A key é a forma que o React usa para identificar cada item da lista de forma única.

Ela ajuda o React a:

  • evitar re-renderizações desnecessárias
  • atualizar apenas os itens que realmente mudaram
  • manter estado e foco corretos em listas dinâmicas

Sem o key, o React até funciona, mas vai mostrar warnings no console do browser.

Nesse exemplo simples:

todo.map((item, index) => (
    <li key={index}>{item}</li>
))
Enter fullscreen mode Exit fullscreen mode

Estamos usando o index como key porque:

  • a lista é simples
  • não tem reordenação
  • não tem inserção no meio
  • não tem remoção
  • não tem estado interno por item

Ou seja: A lista é estática e previsível

porém:

usar index como key NÃO é uma boa prática

O problema aparece quando a lista sofre alterações dinâmicas (remoção, inserção ou reordenação). Quando isso acontece, os índices mudam, mas o React ainda acha que os itens são os mesmos porque a key muda de posição mas não de identidade.

Isso pode causar bugs como:

  • item errado sendo atualizado
  • estado “pulando” de um item para outro
  • inputs perdendo foco
  • comportamento estranho de animações

Por isso a regra geral é:

A key deve identificar o item, não a posição dele.

Nós vamos melhorar isso depois.

Adicionando items ao To-do

Ok ok, eu sei o que você está pensando: "— Temos uma lista, mas não tenho como adicionar itens nela. Grande porcaria!"

Se você parar pra pensar, essa é basicamente a lista de um adolescente comum, considerando que existem estudos hoje que dizem que a adolescencia vai até os 30 anos.

Mas ok, vamos dar um jeito de adicionar novos itens à nossa lista.

Para isso, nós vamos precisar de um <input type="text" .../> onde podemos digitar o texto da nossa tarefa e um <button> com evento onClick={...}que chama uma função que usa o setTodo() do state para modificar a lista de maneira que o React consiga saber dessa alteração.

return (
    <div>
      <h1>My Simple Todo List</h1>
      <div>
        <input type="text" />
        <button>Add Task</button>
      </div>
      <ul>
        {todo.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

Nós vamos precisar também de um jeito de capturar o valor digitado no <input type="text" /> para adiciona-lo à nossa lista. Para isso, vamos declarar um novo state que controla o valor do nosso input. Assim o input se tona um componente controlado e seu valor só muda se o React permitir. Existe um outro modo de ler valores de elementos HTML usando o hook useRef, mas no nosso caso vamos fazer o input ser controlado porquê é mais comum e vai ser mais simples depois.

Vamos criar um novo state no começo da função App():

...
function App() {
    const [inputValue, setInputValue] = useState("");
...
Enter fullscreen mode Exit fullscreen mode

Então vamos conectar esse inputValue com o valor do nosso input:

<div>
    <input type="text" value={inputValue} />
  <button>Add Task</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Se testarmos agora, você vai perceber que não é possível digitar nada no campo input pois ele vai continuar vazio. Isso acontece porque ele só exibe o valor do state inputValue.

Pra nossa sorte, no Javascript existe o evento onChange que é executado cada vez algo muda no nosso input. No nosso caso, cada vez que digitarmos algo, a função onChange será chamada. Perfeito para atualizar o valor do state usando o setInputValue. Dessa forma fechamos o ciclo de controle de estados do nosso input com o state.

Vamos atualizar nosso input adicionando o onChange:

<div>
    <input
      type="text"
    value={inputValue}
    onChange={(e) => setInputValue(e.target.value)}
  />
  <button>Add Task</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Note que quando usamos uma função no onChange, ela recebe automaticamente um argumento: o objeto de evento e.

Esse objeto contém tudo sobre o que aconteceu. Para nós, o detalhe crucial é o e.target.value.

-e.target: é o próprio elemento que disparou o evento.

-.value: é o texto mais recente que está dentro dele no momento da tecla ser pressionada.

É esse valor que passamos para setInputValue(), atualizando o estado e fechando o ciclo: o usuário digita -> o evento captura o novo texto -> o estado é atualizado -> a interface re-renderiza mostrando o novo texto no campo.

(Dica: experimente console.log(e) para ver a quantidade de informações que um evento carrega!)

Se você testar agora, vai notar que o campo texto está funcionando como deveria quando digitamos texto nele. Na realidade, por debaixo dos panos sabemos que ele não está fazendo isso com um campo HTML normal pois ele virou um campo controlado pelo React.

Agora precisamos criar uma função para pegar o valor do inputValue e adicioná-lo à lista do state todo. Moleza!

Mas como adicionamos items a um state tendo como base seu valor anterior?

Eu disse que era moleza. Eu não havia comentado antes mas quando usamos uma função do tipo setState nós podemos passar como argumento dessa função o seu valor atual:

setState( valorAnterior => façaAlgo(valorAnterior))
Enter fullscreen mode Exit fullscreen mode

No nosso caso, como se trata de um array, vamos usar o recurso de array spreading:

setTodo((prev) => [...prev, inputValue]);
Enter fullscreen mode Exit fullscreen mode

Array spreading (...) é uma sintaxe que "expande" um array em elementos individuais, geralmente usado para criar cópias (nosso caso), combinar arrays ou passar múltiplos elementos para funções.

O código completo fica:

import { useState } from "react";
import "./App.css";

function App() {
  const [inputValue, setInputValue] = useState("");

  const [todo, setTodo] = useState([
    "Acordar",
    "Escovar os dentes",
    "Tomar banho",
    "Jogar videogame",
    "Comer",
    "Deitar e olhar para o teto",
    "Dormir",
  ]);

  const handleAddTask = () => {
    setTodo((prev) => [...prev, inputValue]);
  };

  return (
    <div>
      <h1>My Simple Todo List</h1>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={() => handleAddTask()}>Add Task</button>
      </div>
      <ul>
        {todo.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Se testarmos no browser, deve estar assim:

Faltam alguns detalhes:

  • Limpar a caixa de texto depois de adicionar uma tarefa.
  • Não criar uma nova tarefa caso o campo de texto esteja vazio. Fazer nada não é exatamente uma tarefa e você já faz nada normalmente.

Vamos atualizar nossa função handleAddTask():

const handleAddTask = () => {
    if (inputValue == "") return;

    setTodo((prev) => [...prev, inputValue]);
    setInputValue("");
  };
Enter fullscreen mode Exit fullscreen mode

Agora sim, muito melhor.

Agora precisamos de um modo de dizer que uma tarefa foi completada. Seria bom também se tivesse um jeito de remover tarefas. As vezes deitar e olhar para o teto não é tão importante assim.

Foto de tela de um vídeo do canal de youtube React Channel



Deixando as coisas um pouco mais complexas

Do modo que armazenamos nossa lista no momento, um array com as strings das tarefas, fica complicado atualizar quais tarefas já foram feitas ou remover tarefas. Não é impossível, mas é fundamentalmente errado e se um outro programador tiver que alterar seu código ele vai querer bater em você.

Por isso, precisamos deixar o dado de uma tarefa mais robusto. Só a string em sí não é suficiente. Temos que adicionar um marcador ou flag para saber o status da tarefa (um campo booleano) e também precisamos de um identificador único por tarefa (um campo id). Sendo assim, no lugar do array de strings, precisamos criar um array de objects. A estrutura de cada tarefa ficaria assim:

// Task
{
    id: string, // um identificar único por tarefa
    text: string, // o texto da tarefa
    done: boolean // está feita? sim ou não (true | false)
}
Enter fullscreen mode Exit fullscreen mode

Em artigos futuros, vamos transformar essa tarefa (Task) num tipo usando Typescript. Mas no momento vamos deixar assim, bem simples e confiar que não vamos fazer nenhuma besteira com o código que possa quebrar essa estrutura.

Além disso, precisamos de um modo para criar um identificador único para cada tarefa, pois podemos ter tarefas com o mesmo texto em momentos diferentes. Lembre que dissemos que usar o index de um array não é um jeito recomendado para definir key em listas? Essa nova propriedade id seria um candidato ideal para isso.

Criando uma função para gerar ids únicos

Normalmente pra tarefas utilitárias usamos algum módulos de terceiros para pra não ter que ficar reinventando a roda para cada aplicação e porque módulos populares costumam ser amplamente testados. Um módulo popular para criação de ids únicos é o UUID https://www.npmjs.com/package/uuid. Mas no nosso caso, ainda não queremos entrar no assunto de como instalar e usar módulos externos.

Por isso vamos pensar num modo de criar nossa própria função de geração de ids com identificadores que não se repetem.

Um bom candidato para criar um valor que não se repete em javascript é o Date.now(), ele retorna a data com precisão de milisegundos no momento em que foi chamado em formato Unix. Se você for no seu console do browser e digitar Date.now() vai ter algo como:

> Date.now()
< 1767465164625
> Date.now()
< 1767465167310
Enter fullscreen mode Exit fullscreen mode

Como valor da resposta é em milisegundos, é muito difícil que o número seja repetido. Porém não é impossível. Além disso, podemos fazer algo mais legal e com cara de código. Podemos transformar esse número em texto usando base 36 (0-9 + a-z).

Ainda no console do browser, vamos testar adicionando toString(36) no final:

Date.now().toString(36)
'mjync9sr'
Date.now().toString(36)
'mjynceay'
Enter fullscreen mode Exit fullscreen mode

Muito mais legal! Mas como eu disse antes, apesar de difícil, não é impossível que o processamento seja rápido o bastante para que o Date.now() se repita. Pra evitar isso nós podemos adicionar mais um elemento randômico usando o Math.random(). De quebra, vamos transformar o retorno do Math.random() de número para texto também:

> Math.random().toString(36).substring(2,8)
< 'rdqaws'
> Math.random().toString(36).substring(2,8)
< 'm6qfvj'
Enter fullscreen mode Exit fullscreen mode

Juntando os dois, vamos criar uma função generateId antes da declaração do App():

function generateId() {
  return (
    Date.now().toString(36) +
    Math.random().toString(36).substring(2, 8)
  );
}
Enter fullscreen mode Exit fullscreen mode

Com essa função, podemos transformar nosso state todo de um array de strings para um array de objetos e atualizar o resto do código para usar adicionar e exibir as tarefas corretamente:

import { useState } from "react";
import "./App.css";

// Nossa função de gerar IDs
function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}

function App() {
  const [inputValue, setInputValue] = useState("");

    // para cada tarefa chamamos a função
  const [todo, setTodo] = useState([
    { id: generateId(), text: "Acordar", done: false },
    { 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: false },
    { id: generateId(), text: "Deitar e olhar para o teto", done: false },
    { id: generateId(), text: "Dormir", done: false },
  ]);

  const handleAddTask = () => {
    if (inputValue == "") return;

        // atualizada para criar o objeto da tarefa corretamente
    setTodo((prev) => [
      ...prev,
      { id: generateId(), text: inputValue, done: false },
    ]);
    console.log(todo);
    setInputValue("");
  };

  return (
    <div>
      <h1>My Simple Todo List</h1>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={() => handleAddTask()}>Add Task</button>
      </div>
      <ul>
        {todo.map((item) => (
            // agora acessamos o id e o text como propriedades de cada item
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Se testarmos a interface no browser, visualmente nada deve ter mudado, mas precisamos dessas mudanças para o nosso próximo passo:

Criando um componente para a tarefa

Na nossa lista de tarefas, nós precisamos:

  • adicionar um checkbox que mostra se a tarefa foi feita (item.done)
  • mudar o estilo do texto caso a tarefa esteja feita
  • adicionar um botão para remover a tarefa da lista

Poderíamos fazer isso diretamente no meio do código do loop todo.map, mas isso vai dificultar a leitura e não estaremos separando componentes por responsabilidade. Isso abre oportunidade para aprendermos como se cria um novo componente para reuso no React, organizando melhor o código e ter mais controle sobre sua exibição.

Na realidade, a função App() em sí ja é um componente. A única coisa que um componente precisa é de uma função que retorna algo que pode ser interpretado como jsx.

Vamos começar devagar e criar uma função Task() que inicialmente faz a mesma coisa que já estamos fazendo com a lista atual. Antes do App() digite:

function Task(props) {
  return <div>{props.children}</div>;
}
Enter fullscreen mode Exit fullscreen mode

E no App(), dentro do todo.map vamos adicionar o novo componente:

{todo.map((item) => (
     <li key={item.id}>
       <Task>{item.text}</Task>
     </li>
 ))}
Enter fullscreen mode Exit fullscreen mode

Viu que legal? Como nossa função Task() retorna jsx, nós podemos usa-la como se fosse uma nova tag HTML!

Usando props.children

Na função Task(), nos passamos como argumento props e no meio do **jsx **usamos props.children.

Isso é uma propriedade especial que nos permite colocar conteúdo HTML/jsx ou outros componentes entre a abertura e fechamento da tag de declaração do componente. Outras propriedades customizadas podem ser passadas como propriedade dentro da tag do componente. Vamos fazer isso agora com a propriedade done e id:

{todo.map((item) => (
    <li key={item.id}>
      <Task id={item.id} done={item.done}>
          {item.text}
        </Task>
    </li>
))}
Enter fullscreen mode Exit fullscreen mode

Agora, voltando à função Task() vamos adicionar um checkbox que mostra se a tarefa está feita ou não. Mas antes disso, vamos abrir o arquivo App.css e criar uma classe css que alinha os itens da tarefa à esquerda porque ver esse texto centralizado está me deixando irritado.

No final do App.css adicione:

.task-item {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 0.5rem;
  text-align: left;
}
Enter fullscreen mode Exit fullscreen mode

E agora voltando no App.jsx vamos adicionar à Task():

function Task(props) {
  return (
    <div className="task-item">
      <input type="checkbox" checked={props.done} /> | {props.children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Duas coisas coisas foram alteradas:

  1. Adicionamos a classe task-item ao <div>. Mas note que no lugar de usar class usamos className. Isso porque class é uma palavra reservada em javascript, e não se esqueça, apesar de parecer HTML, estamos escrevendo Javascript que se identifica como HTML e é tranformado (transpilado) como tal.

Em React / JSX usamos className para declarar classes CSS

  1. Adicionamos o <input type="checkbox" /> e usamos o props.done em sua propriedade checked para definir se o checkbox estará preenchido ou não.

Vá na sua lista de tarefas e mude a propriedade done de false para true em algum dos itens para ver a diferença no browser:

O problema agora é que se você tentar clicar em algum dos checkboxes vai notar que eles não mudam de estado. Lembra que o mesmo acontecia com o ` para adicionar tarefas?

No entanto, o Task é um componente separado. O todo state não está dentro dele. Como conseguimos fazer alterações num componente pai à partir de um componente filho?

Subindo o nível do state

Imagine que você tem um filho menor que quer ir no cinema. Você não vai dar as chaves do carro para ele ir e voltar. Você o leva até o cinema e pede pra ele te enviar uma mensagem quando o filme acabar dizendo onde ele está exatamente para você ir busca-lo de carro.

Pensando em React, pai define uma função específica e o que ele espera de explicação do filho (argumento): paiVemMeBuscar(localização). E aí o pai vai saber exatamente quando e o quê fazer.

No código, isso significa que o componente pai (App) define uma função (por exemplo, handleToggle) e a passa para o filho (Task) como uma propriedade. Quando a tarefa precisa ser alterada, o filho apenas 'envia a mensagem' (chama a função) com seu id, e o pai executa a mudança de estado real.

Isso se chama "Lifting State Up" (Elevar o Estado), o padrão do React em que o estado é gerenciado no componente pai comum mais próximo.

No App() que é o elemento pai vamos criar a função handleStatusChange:

const handleStatusChange = (id) => { setTodo((prev) => prev.map((item) => item.id === id ? { ...item, done: !item.done } : item ) ); };

Várias coisas pra explicar nessa função. Como fazia Jack, o Estripador, vamos por partes:

  1. O setTodo com função (functional update):
    setTodo((prev) => ...)

    Usamos uma função dentro do setTodo. Ela recebe o valor atual do estado (prev, de previous) como argumento. Isso é a forma mais segura de atualizar um estado que depende do seu valor anterior.

  2. Percorrendo o array com .map():
    prev.map((item) => ...)

    Como prev é um array, usamos o método .map() para criar um novo array, percorrendo item por item. O .map() não altera o array original — ele cria um novo, que é exatamente o que o React precisa para perceber a mudança de estado.

  3. A lógica de atualização (Operador Ternário + Spreading):

    Como a função recebe o id da tarefa a ser alterada, a lógica para cada item é:

    "Este é o item que quero mudar? Se sim, crie uma cópia dele invertendo a propriedade done. Se não, devolva o item sem alterações."

    Isso é feito em uma linha com o operador ternário (condição ? seVerdadeiro : seFalso) e o spread operator (...):

    
      item.id === id ? { ...item, done: !item.done } : item 
    // ↑ Condição       ↑ Se VERDADEIRO                ↑ Se FALSO 
    // (Cria um NOVO objeto (Mantém o item // com `done` invertido) original)
    • { ...item, done: !item.done }: Cria um novo objeto copiando todas as propriedades do item original (...item) e sobrescrevendo a propriedade done com o seu valor invertido (!item.done).
    • !item.done: O operador ! (negação lógica) inverte um valor booleano. Se done for true, vira false, e vice-versa. Isso é um toggle de estado.

Agora só falta o recurso para remover tarefas.

Com tudo o que aprendemos até agora, tenho certeza que você já é capaz de criar esse recurso sozinho. Se quiser pode tentar, e depois compare com o que faremos abaixo:

Adicionando o recurso de remoção de tarefa

Pra remover uma tarefa precisamos de:

  • um botão pra chamar a ação de remoção
  • uma função para esse botão chamar para executar a ação

Lembre-se, a Task não vai se remover sozinha, ela vai chamar uma função do componente pai que vai cuidar disso.

Começando pelo App(), vamos criar a função handleRemoveTask():

const handleRemoveTask = (id) => {
    setTodo((prev) => prev.filter((item) => item.id !== id));
};

Essa função é muito parecida com a handleStatusChange. A diferença é que ela usa outro método maravilhoso dos arrays que é o Array.filter().

// array.filter()
array.filter((item) => condição)

O filter percorre o array e mantém apenas os itens que retornam true na condição.

No nosso caso estamos testando:

"O item.id é diferente do argumento id passado pela função?"

Então passamos essa função como uma propriedade da Task:

{todo.map((item) => (
  <li key={item.id}>
    <Task
      id={item.id}
      done={item.done}
      onStatusChange={handleStatusChange}
      onRemoveTask={handleRemoveTask} // <-- AQUI
    >
      {item.text}
    </Task>
  </li>
))}

E na função Task() adicionamos o botão e chamamos a função onRemoveTask através dele:

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>
  );
}

Por motivos de irritação com o layout fiz umas mudanças no CSS:

.task-item {
  width: 100%;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  text-align: left;
  padding: 0.3rem 0.3rem;
  margin-bottom: 0.3rem;
  background-color: rgba(0, 0, 0, 0.1);
  border-radius: 0.5rem;
}

.task-item div {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.task-item button {
  background: rgba(255, 255, 255, 0.1);
}

.task-item input[type="checkbox"] {
  width: 1.6rem;
  height: 1.6rem;
}

Sendo assim, o código completo ficou:

import { useState } from "react";
import "./App.css";

function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}

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>
  );
}

function App() {
  const [inputValue, setInputValue] = useState("");

  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 = () => {
    if (inputValue == "") return;

    setTodo((prev) => [
      ...prev,
      { id: generateId(), text: inputValue, done: false },
    ]);
    console.log(todo);
    setInputValue("");
  };

  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>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={() => handleAddTask()}>Add Task</button>
      </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;

Captura de tela mostrando a aplicação de lista de tarefas em React funcionando


Você acabou de fazer o seu primeiro App em React! Ele é feio e tosco, mas é seu!

O que eu espero que você tenha aprendido e reflita sobre é:

  • Como fazer o boilerplate do React usando o Vite
  • Como usamos o useState no react para controlar estados da aplicação
  • Como criar novos componentes reutilizáveis
  • Como subir o nível do state (up lift state)

Pense nessas coisas pois no próximo post nós vamos melhorar bastante esse To-do!

Top comments (0)