Controlando múltiplas instâncias dinâmicas com atomFamily e selectorFamily

Controlando múltiplas instâncias dinâmicas com atomFamily e selectorFamily
RODRIGO SILVA HARDER
RODRIGO SILVA HARDER

Compartilhe

Gerenciar estados de uma aplicação React é uma tarefa desafiadora, principalmente quando lidamos com muitos elementos que possuem estados independentes.

Imagine que temos uma loja online chamada "NerdWear" que vende camisetas personalizadas com estampas geek.

No carrinho de compras desta loja, é necessário ter controle sobre a quantidade de cada camiseta e também sobre a quantidade total de camisetas selecionadas.

Além disso, a pessoa consumidora precisa saber qual o valor total da sua compra e qual o valor final de cada item de acordo com a quantidade comprada.

Neste caso, precisamos da ajuda de um gerenciador de estados, para conseguir identificar as mudanças nas quantidades e preços das peças e no valor total dessas variáveis.

Um bom caminho para lidar com essa situação é criar átomos e seletores convencionais do Recoil para cada camiseta, mas, nenhuma loja vende apenas um produto.

Com isso em mente, já pensou na possibilidade de criar manualmente um átomo para cada camiseta da NerdWear? E se ela tivesse mil itens? Seria completamente inviável!

Então, se você ficou curioso(a) para saber como podemos resolver esse problema usando o Recoil, vem comigo que neste artigo vamos:

  • Revisar alguns conceitos importantes do Recoil;
  • Ver exemplos de como esse código da "NerdWear" ficaria usando os átomos e seletores convencionais;
  • Conhecer o atomFamily e o selectorFamily, nossos grandes ajudantes na resolução desse problema;
  • Entender porque e quando devemos usar essas funcionalidades.

Revisando alguns conceitos do Recoil

Antes de seguir e apresentar novos conceitos, é muito importante que os fundamentos do Recoil estejam consolidados.

Por isso, nesse primeiro momento, vamos dar um passo para trás e fazer uma breve revisão sobre esse gerenciador de estados.

O Recoil é uma biblioteca que possui uma arquitetura baseada em átomos e seletores.

Enquanto os átomos são unidades responsáveis por guardar um estado e compartilhar as informações armazenadas com qualquer componente na nossa aplicação, os seletores são gerados a partir da combinação entre átomos ou até entre outros seletores para formar novos valores.

Dessa forma, conseguimos armazenar o mínimo estado possível nos átomos e derivar outros valores com os seletores.

Uma das vantagens do Recoil é sua implementação nos componentes. Quando usamos um hook do React como o useState, criamos um código que tem a seguinte estrutura:

const [valor, setValor] = useState("");

Para usar os átomos e seletores vamos seguir basicamente a mesma estrutura, criar uma constante e atribuir a essa constante um estado através do hook useRecoilState.

Por conta dessa semelhança, essa biblioteca acaba sendo bem familiar para as pessoas que desenvolvem com React.

Além disso, é importante mencionar que para que os estados possam ser compartilhados para todos os componentes é importante englobá-los dentro da tag <RecoilRoot></RecoilRoot>. Normalmente isso é feito no componente principal, o "App", envolvendo todos os demais componentes que ele renderiza.

Banner promocional da Alura, com um design futurista em tons de azul, apresentando o texto

Lidando com os produtos do carrinho usando átomos e seletores convencionais

Agora que já refrescamos nossa memória sobre os principais conceitos do Recoil, vamos entender melhor nosso problema e ver em código tudo aquilo que discutimos lá no começo deste texto.

Então, vem comigo criar o carrinho de compras da "NerdWear" usando apenas os átomos e seletores convencionais do Recoil.

Antes de mais nada é sempre bom ter certeza que a biblioteca foi instalada corretamente para que você consiga usar as funcionalidades disponibilizadas pelo Recoil.

Por isso, lembre-se de usar o comando abaixo, dentro da pasta do projeto, no terminal:

npm install recoil

Feito isso, podemos pensar na seguinte estrutura de pastas e arquivos para o nosso carrinho de compras:

|_src
    |_componentes
        |_ Carrinho
            |_index.jsx
            |_Carrinho.css
        |_ Item
            |_index.jsx
            |_Item.css
    |_state
        |_atom.js
        |_selector.js
    |_App.jsx
    |_App.css

Na pasta "componentes", temos o componente "Carrinho" com todos os produtos, as quantidades por item e a quantidade total de itens, e o componente "Item" que permite adicionar e remover estampas do carrinho.

Na pasta "state", temos um arquivo para armazenar os átomos e outro para armazenar os seletores.

Por fim, como em todo projeto React, temos o arquivo principal, "App.jsx" que renderiza na tela os componentes criados.

Vamos começar a criar os átomos para esse código:

import { atom } from 'recoil';

export const produto1 = atom({
  key: 'produto1',
  default: { quantidade: 0, preco: 20 }
})

export const produto2 = atom({
  key: 'produto2',
  default: { quantidade: 0, preco: 35 }
})

Lembre-se que sempre que for criar um átomo, você precisa de uma chave e um valor padrão.

Neste caso, a chave é o próprio nome da constante que criamos para o átomo e o valor padrão é um objeto que apresenta a quantidade de itens começando com zero e o preço do produto com um valor definido.

No arquivo responsável pelo seletor, iremos usar os valores de cada produto para criar um novo valor que vai representar a soma de todos os itens adicionados no carrinho e o preço total da compra em reais. Isso pode ser feito com o seguinte código:

import { selector } from 'recoil';
import { produto1, produto2 } from './atom';

export const totalDeItens = selector({
  key: 'totalDeItens',
  get: ({ get }) => {
    const p1 = get(produto1).quantidade;
    const p2 = get(produto2).quantidade;
    return p1 + p2
  }
})

export const totalPreco = selector({
  key: 'totalPreco',
  get: ({ get }) => {
    const p1 = get(produto1);
    const p2 = get(produto2);
    const totalP1 = p1.quantidade * p1.preco;
    const totalP2 = p2.quantidade * p2.preco;
    return totalP1 + totalP2
  }
})

Os seletores também precisam de uma chave, mas ao invés de um valor padrão, eles recebem uma propriedade get, responsável por derivar um novo valor a partir do estado atual.

Temos dois seletores um responsável pela quantidade total de itens que recebe a função get para pegar os valores do átomo criado para cada produto e retornamos a soma dos valores das quantidades de cada um (return p1 + p2).

E um outro seletor responsável pelo preço total da compra, que recebe a função get multiplica o preço pela quantidade de cada produto e retorna o valor total (return totalP1 + totalP2).

Agora que temos os átomos e seletores, vamos ver como ficaria o código dos nossos componentes:

O primeiro vai ser o "Item":

import { useRecoilState } from 'recoil';
import { produto1, produto2 } from '../../state/atom';
import './Item.css';

export default function Item({ id }) {
  const [produto, setProduto] = useRecoilState(id === 1 ? produto1 : produto2);

  const precoTotal = produto.quantidade * produto.preco;

  return (
    <div className='item'>
      <h3>Estampa {id}:</h3>
      <p>R$ {precoTotal.toFixed(2)}</p>
      <div>
        <button onClick={() => setProduto({ ...produto, quantidade: produto.quantidade > 0 ? produto.quantidade - 1 : 0 })}>-</button>
        <p>{produto.quantidade}</p>
        <button onClick={() => setProduto({ ...produto, quantidade: produto.quantidade + 1 })}>+</button>
      </div>
    </div>
  );
}
.item{
    display: flex;
    gap:20px;
    justify-content: center;
    align-items: center;
}

.item div{
    display:flex;
    align-items: center;
    justify-content: center;
    gap:10px;
}

.item button{
    background-color: #9AD1D4;
}

No código acima criamos uma constante que armazena o estado atual do produto e uma função que atualiza o valor do estado (setProduto).

Dependendo do id, o estado gerenciado será o do produto1 ou produto2, e isso é feito através de uma lógica condicional.

O retorno do componente é o título do produto (Estampa {id}:), o preço do item que varia de acordo com a quantidade (R$ {precoTotal.toFixed(2)}), um botão com a lógica de adição (() => setProduto({ ...produto, quantidade: produto.quantidade + 1 })), outro com a lógica de remoção (() => setProduto({ ...produto, quantidade: produto.quantidade > 0 ? produto.quantidade - 1 : 0 })) e a quantidade do item selecionado ({quantidade}).

Agora, vamos ver como ficaria o componente "Carrinho":

import { useRecoilValue } from 'recoil';
import { totalDeItens, totalPreco } from '../../state/selector';
import './Carrinho.css';
import Item from '../Item';

export default function Carrinho() {
  const total = useRecoilValue(totalDeItens);
  const precoTotal = useRecoilValue(totalPreco);

  return (
    <div className='carrinho'>
      <h2>Carrinho</h2>
      <Item id={1} />
      <Item id={2} />
      <h3>Itens no carrinho: {total}</h3>
      <h3>Total: R$ {precoTotal.toFixed(2)}</h3>
    </div>
  );
}
.carrinho{
    background-color: #007EA7;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
    width: 400px;
    text-align: center;
}

Esse componente agrupa e renderiza os dois itens "Estampa 1" e "Estampa 2" e mostra o total de itens no carrinho somando as quantidades selecionadas de cada um, e o preço total da compra por meio dos seletores que codamos anteriormente.

Por fim, temos o componente principal, o "App", responsável por renderizar o carrinho na tela:

import { RecoilRoot } from "recoil";
import "./App.css";
import Carrinho from "./componentes/Carrinho";

export default function App() {
  return (
    <RecoilRoot>
      <div className="app">
        <h1>NerdWear</h1>
        <Carrinho />
      </div>
    </RecoilRoot>
  )
}
.app{
    color: #CCDBDC;
    text-align: center;
}

Esse código funciona corretamente, mas lá no comecinho eu mencionei a possibilidade de ter mil estampas na NerdWear e agora que temos nosso código, conseguimos perceber bem ali no arquivo "atom.js" que criar um átomo para cada estampa deixaria o código bem extenso e cheio de repetições desnecessárias, além de ser bem custoso realizar as manutenções necessárias.

Indo além dos átomos e seletores

Por conta desses problemas, fica bem complicado escalar nossa aplicação e ter um código conciso e genérico para supervisionar os estados de todos os itens.

Mas calma, que agora vamos conhecer os heróis que vão nos ajudar a enfrentar essa situação, o atomFamily e o selectorFamily.

atomFamily

Sabe quando você entra na Netflix e se depara com a página com vários perfis? Dependendo de qual você escolher, a plataforma vai abrir um catálogo com o histórico de filmes e séries que a pessoa que usa aquele perfil está assistindo e vai recomendar conteúdos com base nos gostos e no histórico daquele perfil.

Por exemplo, eu adoro filmes de terror e suspense, então no meu perfil teria um histórico repleto de conteúdos desse gênero e indicações com títulos semelhantes.

Cada um desses perfis é independente, mas todos fazem parte da mesma família de usuários no serviço.

E é basicamente assim, que o atomFamily funciona. Você cria apenas um átomo, que consegue gerenciar vários estados na aplicação de forma independente dependendo do parâmetro que você especificar.

Da mesma forma que na Netflix, onde com uma conta, gerenciamos vários perfis independentes uns dos outros.

Vamos voltar para o exemplo do carrinho de compras da "NerdWear". Quando pensamos na solução com os átomos convencionais, codamos um átomo para cada estampa, mas agora vamos fazer algumas alterações nesse átomo.

Para usar o atomFamily precisamos de duas modificações principais.

A primeira modificação diz respeito a estrutura do nosso átomo. Agora criaremos apenas duas constantes: quantidadeDeProdutos e precoDeProdutos, que recebem uma família de átomos responsável por retornar a quantidade de cada produto e o preço de cada item, respectivamente.

Além disso, precisamos especificar que se trata de uma família de átomos, por isso, usaremos o recurso atomFamilyao invés do atomconvencional.

import { atomFamily } from 'recoil';

export const quantidadeDeProdutos = atomFamily({
  key: 'quantidadeDeProdutos',
  default: 0
})

export const precoDeProdutos = atomFamily({
  key: 'precoDeProdutos',
  default: (id) => {
    const precos = {
      1: 20.0,
      2: 35.5
    };
    return precos[id] || 10.0;
  }
})

A segunda mudança é no momento em que vamos usar essas constantes dentro de um componente.

Aqui é necessário informar um parâmetro para que nosso código consiga ter acesso ao estado de um produto específico e possa gerenciar cada estampa individualmente. O código ficaria assim:

import { useRecoilState, useRecoilValue } from 'recoil';
import { quantidadeDeProdutos, precoDeProdutos } from '../../state/atom';
import './Item.css';

export default function Item({ id }) {
  const [quantidade, setQuantidade] = useRecoilState(quantidadeDeProdutos(id));
  const preco = useRecoilValue(precoDeProdutos(id));
//RESTANTE DO CÓDIGO

Com essas alterações, cada produto vai ter um estado com a quantidade e preço independentes, sem precisar criar um atom para cada produto manualmente.

Ao chamar a quantidadeDeProdutos(id) e o precoDeProdutos(id), você vai ter acesso ao estado de cada estampa de forma simplificada, enquanto o trabalho pesado fica por conta do Recoil, que por debaixo dos panos, cria e gerencia esses átomos com base no parâmetro id.

selectorFamily

Se o átomo pode construir uma família, os seletores do Recoil também podem através do recurso selectorFamily, e o funcionamento é bem parecido com o atomFamily.

Basicamente, o selectorFamily é como um selector comum, mas com superpoderes! Ele te permite criar uma família de seletores dinâmicos com base em parâmetros.

Ou seja, em vez de você criar um seletor diferente para cada variação de estado, você cria uma "família de seletores" que aceita parâmetros e, a partir desses parâmetros, retorna o estado desejado.

Agora vamos usar a família de átomos que criamos em um selectorFamily.

Inicialmente vamos alterar a lógica de ambos os seletores para tornar o código mais limpo e genérico.

import { selectorFamily } from 'recoil';
import { quantidadeDeProdutos, precoDeProdutos } from './atom';

export const totalDeItens = selectorFamily({
  key: 'totalDeItens',
  get: (idDosProdutos) => ({ get }) => {
    return idDosProdutos.reduce((total, id) => total + get(quantidadeDeProdutos(id)), 0)
  }
});

export const valorTotalCarrinho = selectorFamily({
  key: 'valorTotalCarrinho',
  get: (idDosProdutos) => ({ get }) => {
    return idDosProdutos.reduce((total, id) => {
      const quantidade = get(quantidadeDeProdutos(id));
      const preco = get(precoDeProdutos(id));
      return total + quantidade * preco;
    }, 0)
  }
})

No seletor totalDeItens ao invés de salvar a quantidade de cada produto em uma constante e depois somar tudo ao final, vamos pegar uma lista de ID's (idDosProdutos) e interar sobre essa lista para pegar o valor da quantidade de um determinado item e adicionar ao valor total que começa com o valor zero.

Já no seletor valorTotalCarrinho também informamos o parâmetro idDosProdutos que contém a lista de ID's.

A lista será percorrida pelo reduce e para cada item, vai armazenar o valor da quantidade e do preço em constantes intuitivamente chamada de quantidade e preco.

Por fim, o valor da multiplicação da quantidade pelo preço de cada item será acrescido ao valor total que começa como zero.

Da mesma forma que no atomFamily, precisamos informar que se trata de uma família de seletores e por isso precisamos usar o recurso selectorFamily.

Agora vamos fazer as alterações no componente "Carrinho" para refletir as mudanças que fizemos na forma de manipular o estado dos produtos do nosso carrinho de compras.

import { useRecoilValue } from 'recoil';
import { totalDeItens, valorTotalCarrinho } from '../../state/selector';
import Item from '../Item';
import './Carrinho.css';

export default function Carrinho() {
  const idProdutos = [1, 2];
  const totalItens = useRecoilValue(totalDeItens(idProdutos));
  const valorTotal = useRecoilValue(valorTotalCarrinho(idProdutos));

  return (
    <div className='carrinho'>
      <h2>Carrinho</h2>
      {idProdutos.map((id) => (
        <Item key={id} id={id} />
      ))}
//RESTANTE DO CÓDIGO

Agora, ao invés de renderizar o componente "Item" várias vezes, eu faço um map sobre a lista de ID's (idProdutos) e ao ter acesso aos seletores eu informo como parâmetro essa mesma lista de ID's.

Quando usamos o selectorFamily conseguimos passar parâmetros personalizados como uma lista de ID's de produtos, para calcular valores específicos, o que não seria possível usando o selector convencional.

Ao tentar implementar apenas o selector teríamos a seguinte mensagem de erro no console do navegador: "totalDeItens is not a function", que ocorreria por conta do não reconhecimento do parâmetro idDosProdutos.

Com a possibilidade de usar parâmetros, que vem junto com o selectorFamily, conseguimos tornar o código mais dinâmico e evitar duplicações indesejadas, o que traz melhorias tanto para o desempenho quanto para a manutenção do código.

Por que e onde usar atomFamily e selectorFamily?

Toda essa mudança vem com novas possibilidades de expandir o gerenciamento de estado e tornar ele ainda mais eficiente.

Vem comigo entender os benefícios que o atomFamily e o selectorFamily trazem para sua aplicação e conseguir responder de vez a pergunta: Por que usar esses recursos do Recoil?

Como vimos ao longo do código que desenvolvemos para o carrinho de compras da "NerdWear", o atomFamilye o selectorFamily foram indispensáveis para tornar o código mais dinâmico e escalável, pois permitiu a criação de uma família de estados acessados dinamicamente com base em um parâmetro, como o ID, evitando criar manualmente os átomos e seletores independentes para cada instância.

Devido a dinamicidade na criação dos átomos e seletores, dizemos adeus as duplicações de código para criar o estado inicial para a quantidade e preço de um produto e nos despedimos também das várias constantes que criamos para calcular o valor total da compra ou saber a quantidade de itens no carrinho.

Essa redução de código vem acompanhada com uma simplificação na manutenção do código que fica mais enxuto e compreensível.

Além de tudo isso, temos a possibilidade de reutilização mais presente, pois precisamos criar apenas uma instância e ao definir o parâmetro, o Recoil por conta própria nos devolve um estado para cada produto, sem precisar definir isso explicitamente no arquivo que gerencia os estados compartilhados.

Outra grande vantagem do Recoil é que ele mantém na memória apenas os estados que estão sendo utilizados.

Com o atomFamily e selectorFamily, você só mantém em uso os estados dinâmicos de componentes ou dados que estão ativos, sem sobrecarregar a aplicação com informações desnecessárias.

Isso melhora significativamente a performance, especialmente em aplicações com grande volume de dados.

Com todos esses pontos e com os exemplos que trouxemos ao longo do texto acredito que tenha ficado mais claro o motivo de optarmos por usar atomFamily e selectorFamily para a construção do gerenciamento de estados da nossa aplicação.

Mas fica a pergunta, onde mais podemos usar esse recurso? Calma que vamos conversar sobre isso agora mesmo!

Um exemplo clássico é em um projeto que possua uma lista de tarefas. Você pode criar um estado dinâmico para cada tarefa, com informações como descrição, status (completa/incompleta) e prioridade.

//Cria uma família de átomos para gerenciar as tarefas da lista
const tarefaState = atomFamily({
  key: 'tarefaState',
  default: { descricao: '', concluida: false, prioridade: 'low' }
});

// Gerenciar estado da tarefa com ID '123'
const tarefa = useRecoilValue(tarefaState(123));

// Alterar o status de conclusão da tarefa
const setTarefa = useSetRecoilState(tarefaState(123));
setTarefa((task) => ({ ...tarefa, concluida: !tarefa.concluida }))

Se sua aplicação contém formulários dinâmicos com múltiplos campos, como um cadastro de usuário com nome, e-mail, senha, etc., você pode usar o atomFamily para gerenciar o estado de cada campo, sem precisar criar estados individuais para cada campo do formulário.

const camposDoFormulario = atomFamily({
  key: 'camposDoFormulario',
  default: ''
});

// Gerenciar o valor do campo 'email' no formulário de ID 'formulario1'
const valorDoEmail = useRecoilValue(camposDoFormulario({ idDoFormulario: 'formulario1', campo: 'email' }))

Em uma aplicação de mensagens ou chat, o selectorFamily pode ser usado para obter dinamicamente a lista de mensagens de um chat específico, com base no ID da conversa.

const mensagensState = atomFamily({
  key: 'mensagensState',
  default: []
})

const mensagensDoChatSelector = selectorFamily({
  key: 'mensagensDoChatSelector',
  get: (idDoChat) => ({ get }) => {
    return get(mensagensState(idDoChat));
  }
})

// Usando o selector para obter as mensagens do chat com ID '123'
const mensagens = useRecoilValue(mensagensDoChatSelector ('123'));

Um outro exemplo é uma lista de produtos que precisamos filtrar com base em diferentes critérios, como categoria, preço ou disponibilidade. O selectorFamily pode ajudar a exibir a lista filtrada dinamicamente com base nos parâmetros informados.

const produtosState = atom({
  key: 'produtosState',
  default: [
    { id: 1, nome: 'Produto A', categoria: 'Eletrônicos', preco: 100 },
    { id: 2, nome: 'Produto B', categoria: 'Móveis', preco: 300 },
    // ... OUTROS PRODUTOS
  ]
})

const produtosFiltradosSelector = selectorFamily({
  key: 'produtosFiltradosSelector',
  get: (filter) => ({ get }) => {
    const produtos = get(produtosState);
    return produtos.filter(produto => produto.categoria === filter.categoria);
  }
})

// Usando o selector para obter produtos da categoria 'Eletrônicos'
const produtosEletronicos = useRecoilValue(produtosFiltradosSelectorr({ categoria: 'Eletrônicos' }))

Esses são apenas alguns exemplos que ilustram as possibilidades de uso do atomFamilye do selectorFamily.

Mas no geral, sempre que você precisar de vários átomos para representar diferentes instâncias de um mesmo tipo de estado, pode usar o atomFamily e quando você precisar de cálculos derivados ou transformar o estado com base em parâmetros dinâmicos, pode usar o selectorFamily.

Integrando nosso e-commerce com um servidor

Até agora, exploramos apenas o gerenciamento do estado local da nossa aplicação. No mundo real, um e-commerce precisa manter o estado do carrinho atualizado em tempo real com o servidor para garantir que as ações do(a) usuário(a) reflitam corretamente no back-end.

Para isso, podemos combinar o estado local (gerenciado pelo Recoil) com API's que façam atualizações no servidor.

Sempre que a pessoa usuária realizar uma ação, como adicionar um item ao carrinho, podemos não apenas atualizar o estado local com atom ou atomFamily, mas também fazer uma requisição HTTP para o servidor atualizar a base de dados de forma síncrona.

Caso queira se aprofundar sobre o que é HTTP, temos o artigo da Akemi com um guia completo sobre o que é e como funciona o protocolo da web.

Então, vem comigo atualizar o código do carrinho de compras da "NerdWear" para que ele consiga pegar os valores dos estados que estão armazenados nos átomos e seletores do nosso gerenciador de estados e usar para atualizar os valores automaticamente no servidor.

Para conseguir visualizar esse processo acontecendo sem a necessidade de hospedar um servidor na web ou mesmo usar recursos mais avançados, vamos fazer uso do bom e velho "json-server" para simular uma API.

Além disso, também vamos usar a biblioteca axios para nos ajudar a fazer as requisições HTTP de maneira simplificada que precisaremos para adicionar, atualizar e remover itens do carrinho.

Caso queira saber mais sobre essas duas ferramentas incríveis, temos dois artigos escritos pelo Vinny Neves que podem ser bem valiosos para você:

Agora que já sabemos tudo que vamos usar, bora implementar essas alterações!

Inicialmente, é necessário instalar essas ferramentas no terminal, e podemos usar o comando:

npm install json-server axios

Dessa forma, já instalamos tudo que iremos usar de uma única vez. Em seguida vamos criar na raiz do nosso projeto um arquivo chamado "db.json" que vai abrigar o carrinho de compras que vai estar lá no backend.

{
  "carrinho": [
  ]
}

A princípio este carrinho pode ficar vazio, pois como disse vamos inserir, atualizar e remover itens dele, tudo de maneira automática.

É importante lembrar que precisamos rodar essa nossa API no terminal para que nosso código funcione corretamente. Por isso, precisamos realizar o seguinte comando:

json-server --watch db.json --port 3001

Nesse caso eu especifiquei a porta 3001 do localhost, mas você pode ficar a vontade para especificar outra porta ou até mesmo para não especificar nenhuma porta, mas é importante lembrar que essa entrada vai ser utilizada para termos acesso às informações do backend, então fique atento(a) em qual porta você vai rodar sua API.

Em seguida, vamos criar uma pasta dentro de "src" que vai se chamar "api" e dentro dela vamos criar o arquivo "carrinhoAPI.js" que vai abrigar todo a manipulação que faremos no backend.

import axios from 'axios';

const baseURL = 'http://localhost:3001/carrinho';

export const obterCarrinho = async () => {
  const resposta = await axios.get(baseURL);
  return resposta.data;
};

export const adicionarItemAoCarrinho = async (id, quantidade) => {
  await axios.post(baseURL, {
    id: String(id),
    quantidade,
  });
  console.log("Adição bem-sucedida:", {
    id: String(id),
    quantidade,
  });
};

export const atualizarItemNoCarrinho = async (itemExistente, novaQuantidade) => {
  await axios.put(`${baseURL}/${itemExistente.id}`, {
    ...itemExistente,
    quantidade: novaQuantidade,
  });
  console.log("Atualização bem-sucedida:", {
    ...itemExistente,
    quantidade: novaQuantidade,
  });
};

export const removerItemDoCarrinho = async (id) => {
  await axios.delete(`${baseURL}/${id}`);
  console.log(`Item ${id} removido do carrinho.`);
};

No código acima, criamos algumas funções importantes para manipular nossa API, dentre eles, a função obterCarrinho que usa o método get para ter acesso às informações da nossa API, a função adicionarItemAoCarrinho, que usa o método post para acrescentar uma informação nova dentro da API, a função atualizarItemNoCarrinho que atualiza valores existentes no carrinho através do método put e a função removerItemDoCarrinho que remove itens do carrinho pelo método delete.

Em todos os casos temos alguns console.log espalhados que nos ajudam a identificar no navegador se as informações foram corretamente modificadas.

E para finalizar, precisamos realizar algumas alterações no código do componente "Item" para que ele sincronize com as mudanças locais que estão sendo feitas na interface do projeto.

Nele vamos acrescentar a função atualizaCarrinho que vai conter a seguinte lógica:

  const atualizarCarrinho = async (novaQuantidade) => {
    try {
      // Atualiza o estado local
      setQuantidade(novaQuantidade);

      // Faz uma requisição para obter o carrinho atual
      const resposta = await obterCarrinho();
      const itemExistente = resposta.find(item => item.id === String(id)); // Verifica se o item existe

      if (itemExistente) {
        // Se a nova quantidade for 0, remove o item do carrinho
        if (novaQuantidade <= 0) {
          await removerItemDoCarrinho(itemExistente.id);
        } else {
          // Atualiza a quantidade do item existente
          await atualizarItemNoCarrinho(itemExistente, novaQuantidade);
        }
      } else {
        // Se não existir, adiciona um novo item ao carrinho
        await adicionarItemAoCarrinho(id, novaQuantidade);
      }
    } catch (error) {
      console.error("Erro ao atualizar o carrinho:", error.response?.data || error.message);
    }
  }

No código acima, atualizamos a nossa interface em um primeiro momento e simultaneamente, atualizamos nossa API com as quantidades que foram adicionadas ou removidas de cada camiseta, por meio de uma lógica condicional.

O resultado é um projeto que consegue atualizar tanto a interface do(a) usuário(a) quanto o servidor automaticamente e em tempo real.

Gif. Tela do carrinho da NerdWear no navegador mostrando a adição e remoção dos dois itens e o console do navegador mostrando mensagens que refletiam essas ações e a quantidade do produto que estava sendo modificado, juntamente com o ID.
Imagem. Arquivo "db.json" após as atualizações feitas no gif acima, mostrando que alguns itens foram acrescentados para os dois itens.

Expandindo o negócio

Acabamos de integrar o carrinho de compras da NerdWear com uma API mockada para simular a atualização dos dados no servidor em tempo real. Incrível, né?

Mas esse compartilhamento de estados com a nossa API não é tudo que o Recoil junto com seus átomos e seletores pode fazer.

Também podemos explorar mais a regra de negócios do nosso e-commerce, e implementar ao carrinho de compras outras funcionalidades como validação da quantidade máxima de itens, algo essencial para que uma única pessoa não consiga comprar todo nosso estoque!

Para conseguir aplicar essa limitação de itens no carrinho, uma opção é criar um átomo que guarda o valor máximo que uma pessoa pode adicionar à sua compra.

Em seguida, codamos um seletor que pega esse átomo que guarda o valor máximo com o método get e aplica uma lógica para verificar se o valor atual presente no carrinho é inferior a ele.

Depois disso, para adicionar essa alteração no nosso projeto, podemos aplicar uma condicional (if/else) usando esse seletor juntamente com o parâmetro que abriga o valor máximo na função atualizarCarrinho que está dentro do componente "Item" para impedir que a pessoa acrescente mais itens no carrinho e também para exibir uma mensagem na tela para informar para o(a) usuário(a) que ele atingiu o valor máximo de itens no carrinho.

Outra regra bacana para implementar em um e-commerce, principalmente em datas comemorativas, é o uso de cupons de desconto.

Podemos implementar essa ideia em duas partes, uma pensando em validar o cupom de desconto e outra focada em aplicar o desconto no valor total da compra.

Para isso, uma opção é usar nosso bom e velho amigo Recoil para criar um átomo que guarde um objeto que contém uma string que armazena o código de desconto e inicialmente começa vazia e um booleano que inicia com o valor false para indicar que o cupom ainda não foi aplicado.

A partir desse estado vamos criar um seletor que vai validar o cupom de desconto e exibir uma mensagem de acordo com essa validação.

E em seguida precisamos alterar o seletor responsável pelo valor total do carrinho para recalcular o valor aplicando o desconto e podemos fazer isso com base no seletor de validação do cupom.

E para implementar esses valores armazenados nos átomos e seletores, precisamos criar no componente "Carrinho" um campo de input para que a pessoa adicione o cupom e um botão com a lógica que observa a mudança no estado do input e aplica o cupom.

Além do desconto, em muitos carrinhos de compra, o (a) cliente pode mudar o tamanho, a cor e até mesmo outras opções oferecidas pelo produto, e na NerdWear também podemos implementar essas funcionalidade com o Recoil.

Aqui podemos criar dois novos átomos que vão armazenar um estado inicial como tamanho "M" e cor "Branca" e no nosso componente "Item", podemos usar caixas de seleção para alterar as opções padrão.

Essas são apenas algumas das implementações que conseguimos fazer no carrinho do nosso e-commerce, mas existem muitas outras.

Caso você queira praticar e se desafiar, pode usar essas funcionalidades que mencionei e até mesmo as dicas de como eu faria para ampliar o seu projeto e explorar ainda mais os recursos que o Recoil disponibiliza.

Tenho certeza que vai ser uma experiência enriquecedora e cheia de aprendizados!

Caso queira consultar o código completo do projeto deixo o link do repositório no GitHub com todos os código que fizemos até aqui.

Conclusão

Chegamos ao final desse texto e com ele conseguimos conhecer mais alguns recursos do Recoil, essa biblioteca tão interessante que nos ajuda no gerenciamento de estado em projetos React.

Construímos juntos o carrinho de compras da NerdWear, nossa loja de camisetas geek e fizemos um comparativo super completo entre o uso do atom e selector convencional e o uso do atomFamilye selectorFamily para gerenciar os estados de quantidade e preço de cada camiseta e conseguimos compreender as vantagens e benefícios que o atomFamily e o selectorFamily tem em detrimento ao atom e selector convencionais.

Também vimos outros exemplos de uso desses recursos, que podem fazer parte do seu dia a dia enquanto pessoa desenvolvedora Front-end.

Além disso, avançamos ainda mais no projeto da NerdWear e implementamos um servidor mockado para simular como um e-commerce funcionaria na prática usando o json-server e a biblioteca axios e vimos mais potencialidades para expandir o negócio e adicionar outras funcionalidades e recursos para melhorar a experiência do usuário.

Agora te convido a praticar tudo isso que vimos no projeto da NerdWear e se quiser implementar ainda mais funcionalidades para deixar nosso carrinho de compras ainda mais completo. Você aceita o desafio?

E caso queira mergulhar ainda mais fundo no React e na biblioteca Recoil, você pode consultar os links abaixo:

Nos vemos na próxima!

RODRIGO SILVA HARDER
RODRIGO SILVA HARDER

Graduado em Design Gráfico, Técnico em Química e Licenciatura em química, Rodrigo é apaixonado por ensinar e aprender e busca se aventurar em diferentes linguagens de programação. Faço parte da escola semente e atuo como monitor no time de Fórum Ops.

Veja outros artigos sobre Front-end