Gerenciando Estado Complexo

Módulo 3: Estado e Ciclo de Vida

Aula 3
1

Estado com Objetos e Arrays

Aprenda a gerenciar estado com estruturas de dados complexas

Estado com Objetos e Arrays

Princípio da Imutabilidade

No React, o estado deve ser tratado como imutável. Sempre crie uma nova cópia ao atualizar.

Trabalhando com Objetos

Atualizando Propriedades

function PerfilUsuario() {
  const [usuario, setUsuario] = useState({
    nome: 'João',
    email: 'joao@email.com',
    idade: 25,
    endereco: {
      cidade: 'São Paulo',
      estado: 'SP'
    }
  });

  // Atualizar uma propriedade
  const atualizarNome = (novoNome) => {
    setUsuario(prevUsuario => ({
      ...prevUsuario,
      nome: novoNome
    }));
  };

  // Atualizar propriedade aninhada
  const atualizarCidade = (novaCidade) => {
    setUsuario(prevUsuario => ({
      ...prevUsuario,
      endereco: {
        ...prevUsuario.endereco,
        cidade: novaCidade
      }
    }));
  };

  return (
    <div>
      <h2>{usuario.nome}</h2>
      <p>{usuario.email}</p>
      <p>{usuario.endereco.cidade}, {usuario.endereco.estado}</p>
    </div>
  );
}

Trabalhando com Arrays

Operações Comuns

function ListaTarefas() {
  const [tarefas, setTarefas] = useState([
    { id: 1, texto: 'Estudar React', concluida: false },
    { id: 2, texto: 'Fazer exercícios', concluida: true }
  ]);

  // Adicionar item
  const adicionarTarefa = (texto) => {
    const novaTarefa = {
      id: Date.now(),
      texto,
      concluida: false
    };
    setTarefas(prevTarefas => [...prevTarefas, novaTarefa]);
  };

  // Remover item
  const removerTarefa = (id) => {
    setTarefas(prevTarefas => 
      prevTarefas.filter(tarefa => tarefa.id !== id)
    );
  };

  // Atualizar item
  const toggleTarefa = (id) => {
    setTarefas(prevTarefas =>
      prevTarefas.map(tarefa =>
        tarefa.id === id
          ? { ...tarefa, concluida: !tarefa.concluida }
          : tarefa
      )
    );
  };

  // Ordenar array
  const ordenarPorStatus = () => {
    setTarefas(prevTarefas => 
      [...prevTarefas].sort((a, b) => a.concluida - b.concluida)
    );
  };

  return (
    <div>
      {tarefas.map(tarefa => (
        <div key={tarefa.id}>
          <input
            type="checkbox"
            checked={tarefa.concluida}
            onChange={() => toggleTarefa(tarefa.id)}
          />
          <span>{tarefa.texto}</span>
          <button onClick={() => removerTarefa(tarefa.id)}>X</button>
        </div>
      ))}
    </div>
  );
}

Estado com Múltiplas Estruturas

function CarrinhoCompras() {
  const [carrinho, setCarrinho] = useState({
    itens: [],
    cupomDesconto: null,
    enderecoEntrega: {
      rua: '',
      cidade: '',
      cep: ''
    }
  });

  // Adicionar item ao carrinho
  const adicionarItem = (produto) => {
    setCarrinho(prevCarrinho => {
      const itemExistente = prevCarrinho.itens.find(
        item => item.id === produto.id
      );

      if (itemExistente) {
        // Incrementa quantidade se já existe
        return {
          ...prevCarrinho,
          itens: prevCarrinho.itens.map(item =>
            item.id === produto.id
              ? { ...item, quantidade: item.quantidade + 1 }
              : item
          )
        };
      } else {
        // Adiciona novo item
        return {
          ...prevCarrinho,
          itens: [...prevCarrinho.itens, { ...produto, quantidade: 1 }]
        };
      }
    });
  };

  // Aplicar cupom
  const aplicarCupom = (cupom) => {
    setCarrinho(prevCarrinho => ({
      ...prevCarrinho,
      cupomDesconto: cupom
    }));
  };

  // Atualizar endereço
  const atualizarEndereco = (campo, valor) => {
    setCarrinho(prevCarrinho => ({
      ...prevCarrinho,
      enderecoEntrega: {
        ...prevCarrinho.enderecoEntrega,
        [campo]: valor
      }
    }));
  };

  // Calcular total
  const calcularTotal = () => {
    const subtotal = carrinho.itens.reduce(
      (total, item) => total + (item.preco * item.quantidade),
      0
    );
    
    if (carrinho.cupomDesconto) {
      return subtotal * (1 - carrinho.cupomDesconto.percentual);
    }
    
    return subtotal;
  };

  return (
    <div>
      {/* Interface do carrinho */}
    </div>
  );
}

Padrões de Atualização Imutável

1. Spread Operator

// Objeto
const novoObjeto = { ...objetoAntigo, propriedade: novoValor };

// Array
const novoArray = [...arrayAntigo, novoItem];

2. Métodos de Array que Retornam Novo Array

// filter - remove itens
const filtrado = array.filter(item => item.id !== idParaRemover);

// map - atualiza itens
const atualizado = array.map(item => 
  item.id === idParaAtualizar ? { ...item, ...novosDados } : item
);

// concat - adiciona itens
const comNovosItens = array.concat(novosItens);

3. Evite Mutações

// ❌ ERRADO - muta o array original
array.push(novoItem);
array[0].nome = 'Novo Nome';
array.sort();

// ✅ CORRETO - cria novo array
setArray([...array, novoItem]);
setArray(array.map((item, i) => i === 0 ? { ...item, nome: 'Novo Nome' } : item));
setArray([...array].sort());

Dicas de Performance

1. Use Funções de Atualização

// Evita problemas de fechamento (closure)
setEstado(prevEstado => {
  // Use prevEstado ao invés de estado
  return novoEstado;
});

2. Normalize Dados Complexos

// Ao invés de arrays aninhados
const [dados, setDados] = useState([
  { id: 1, nome: 'Item', subitens: [{ id: 2, nome: 'Subitem' }] }
]);

// Considere normalizar
const [itens, setItens] = useState({
  1: { id: 1, nome: 'Item' },
  2: { id: 2, nome: 'Subitem', parentId: 1 }
});

3. Separe Estados Independentes

// Ao invés de um grande objeto
const [estado, setEstado] = useState({
  usuario: {},
  carrinho: [],
  configuracoes: {}
});

// Prefira estados separados
const [usuario, setUsuario] = useState({});
const [carrinho, setCarrinho] = useState([]);
const [configuracoes, setConfiguracoes] = useState({});
2

Introdução ao useReducer

18:00

Quando e como usar useReducer para estado complexo

3

Projeto: Carrinho de Compras

Implemente um carrinho de compras completo usando estado complexo

Activity

Projeto: Carrinho de Compras

Objetivo

Criar um carrinho de compras funcional que demonstre o gerenciamento de estado complexo.

Funcionalidades Requeridas

  1. Lista de Produtos

    • Exibir produtos com nome, preço e imagem
    • Botão para adicionar ao carrinho
  2. Carrinho

    • Mostrar itens adicionados
    • Quantidade de cada item
    • Botões para aumentar/diminuir quantidade
    • Botão para remover item
    • Calcular subtotal por item
    • Calcular total geral
  3. Recursos Extras

    • Contador de itens no carrinho
    • Mensagem quando carrinho está vazio
    • Persistência no localStorage

Estrutura de Dados

// Produtos disponíveis
const produtosDisponiveis = [
  {
    id: 1,
    nome: 'Notebook',
    preco: 2999.90,
    imagem: 'https://via.placeholder.com/200'
  },
  {
    id: 2,
    nome: 'Mouse Gamer',
    preco: 199.90,
    imagem: 'https://via.placeholder.com/200'
  },
  // ... mais produtos
];

// Estado do carrinho
const carrinhoInicial = {
  itens: [],
  // itens terá estrutura: { id, nome, preco, quantidade }
};

Template Inicial

import React, { useState, useEffect } from 'react';

function CarrinhoDeCompras() {
  const produtosDisponiveis = [
    { id: 1, nome: 'Notebook', preco: 2999.90 },
    { id: 2, nome: 'Mouse Gamer', preco: 199.90 },
    { id: 3, nome: 'Teclado Mecânico', preco: 399.90 },
    { id: 4, nome: 'Monitor 24"', preco: 899.90 },
    { id: 5, nome: 'Headset', preco: 299.90 }
  ];

  // Seu código aqui

  return (
    <div>
      <h1>Loja Virtual</h1>
      {/* Implemente a interface aqui */}
    </div>
  );
}

export default CarrinhoDeCompras;

Dicas de Implementação

1. Adicionar ao Carrinho

const adicionarAoCarrinho = (produto) => {
  // Verificar se produto já existe no carrinho
  // Se sim, incrementar quantidade
  // Se não, adicionar com quantidade 1
};

2. Atualizar Quantidade

const atualizarQuantidade = (id, novaQuantidade) => {
  // Se quantidade for 0, remover do carrinho
  // Senão, atualizar quantidade
};

3. Calcular Total

const calcularTotal = () => {
  return carrinho.itens.reduce((total, item) => {
    return total + (item.preco * item.quantidade);
  }, 0);
};

Solução Completa

Clique para ver a solução
import React, { useState, useEffect } from 'react';

function CarrinhoDeCompras() {
  const produtosDisponiveis = [
    { id: 1, nome: 'Notebook', preco: 2999.90 },
    { id: 2, nome: 'Mouse Gamer', preco: 199.90 },
    { id: 3, nome: 'Teclado Mecânico', preco: 399.90 },
    { id: 4, nome: 'Monitor 24"', preco: 899.90 },
    { id: 5, nome: 'Headset', preco: 299.90 }
  ];

  // Inicializa carrinho do localStorage ou vazio
  const [carrinho, setCarrinho] = useState(() => {
    const carrinhoSalvo = localStorage.getItem('carrinho');
    return carrinhoSalvo ? JSON.parse(carrinhoSalvo) : { itens: [] };
  });

  // Salva carrinho no localStorage quando muda
  useEffect(() => {
    localStorage.setItem('carrinho', JSON.stringify(carrinho));
  }, [carrinho]);

  const adicionarAoCarrinho = (produto) => {
    setCarrinho(prevCarrinho => {
      const itemExistente = prevCarrinho.itens.find(item => item.id === produto.id);
      
      if (itemExistente) {
        return {
          ...prevCarrinho,
          itens: prevCarrinho.itens.map(item =>
            item.id === produto.id
              ? { ...item, quantidade: item.quantidade + 1 }
              : item
          )
        };
      } else {
        return {
          ...prevCarrinho,
          itens: [...prevCarrinho.itens, { ...produto, quantidade: 1 }]
        };
      }
    });
  };

  const atualizarQuantidade = (id, novaQuantidade) => {
    if (novaQuantidade <= 0) {
      removerDoCarrinho(id);
    } else {
      setCarrinho(prevCarrinho => ({
        ...prevCarrinho,
        itens: prevCarrinho.itens.map(item =>
          item.id === id
            ? { ...item, quantidade: novaQuantidade }
            : item
        )
      }));
    }
  };

  const removerDoCarrinho = (id) => {
    setCarrinho(prevCarrinho => ({
      ...prevCarrinho,
      itens: prevCarrinho.itens.filter(item => item.id !== id)
    }));
  };

  const limparCarrinho = () => {
    setCarrinho({ itens: [] });
  };

  const calcularTotal = () => {
    return carrinho.itens.reduce((total, item) => {
      return total + (item.preco * item.quantidade);
    }, 0);
  };

  const contarItens = () => {
    return carrinho.itens.reduce((total, item) => total + item.quantidade, 0);
  };

  return (
    <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '20px' }}>
      <h1>Loja Virtual</h1>
      
      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '20px' }}>
        {/* Lista de Produtos */}
        <div>
          <h2>Produtos Disponíveis</h2>
          <div style={{ display: 'grid', gap: '10px' }}>
            {produtosDisponiveis.map(produto => (
              <div key={produto.id} style={{
                border: '1px solid #ddd',
                padding: '10px',
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center'
              }}>
                <div>
                  <h3>{produto.nome}</h3>
                  <p>R$ {produto.preco.toFixed(2)}</p>
                </div>
                <button onClick={() => adicionarAoCarrinho(produto)}>
                  Adicionar ao Carrinho
                </button>
              </div>
            ))}
          </div>
        </div>

        {/* Carrinho */}
        <div style={{ 
          border: '1px solid #ddd', 
          padding: '20px',
          position: 'sticky',
          top: '20px',
          height: 'fit-content'
        }}>
          <h2>Carrinho ({contarItens()} itens)</h2>
          
          {carrinho.itens.length === 0 ? (
            <p>Seu carrinho está vazio</p>
          ) : (
            <>
              {carrinho.itens.map(item => (
                <div key={item.id} style={{
                  borderBottom: '1px solid #eee',
                  paddingBottom: '10px',
                  marginBottom: '10px'
                }}>
                  <h4>{item.nome}</h4>
                  <p>R$ {item.preco.toFixed(2)} cada</p>
                  <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
                    <button onClick={() => atualizarQuantidade(item.id, item.quantidade - 1)}>
                      -
                    </button>
                    <span>{item.quantidade}</span>
                    <button onClick={() => atualizarQuantidade(item.id, item.quantidade + 1)}>
                      +
                    </button>
                    <button onClick={() => removerDoCarrinho(item.id)}>
                      Remover
                    </button>
                  </div>
                  <p>Subtotal: R$ {(item.preco * item.quantidade).toFixed(2)}</p>
                </div>
              ))}
              
              <div style={{ 
                marginTop: '20px', 
                paddingTop: '20px', 
                borderTop: '2px solid #333' 
              }}>
                <h3>Total: R$ {calcularTotal().toFixed(2)}</h3>
                <button onClick={limparCarrinho} style={{ marginTop: '10px' }}>
                  Limpar Carrinho
                </button>
              </div>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

export default CarrinhoDeCompras;

Desafios Extras

  1. Sistema de Cupons: Adicione campo para inserir cupom de desconto
  2. Filtros: Implemente filtros por preço ou categoria
  3. Busca: Adicione busca por nome de produto
  4. Wishlist: Crie lista de desejos
  5. Animações: Adicione transições suaves ao adicionar/remover itens
3 content items