Escolhendo a Estrutura do Estado

Estruturar bem o estado pode fazer a diferença entre um componente que é agradável de modificar e depurar, e um que é uma fonte constante de erros. Aqui estão algumas dicas que você deve considerar ao estruturar estados.

Você aprenderá

  • Quando usar uma única variável de estado versus várias
  • O que evitar ao organizar estados
  • Como corrigir problemas comuns na estrutura do estado

Princípios para estruturar estados

Quando você escreve um componente que mantém algum estado, você terá que fazer escolhas sobre quantas variáveis de estado usar e qual deve ser a forma dos dados. Embora seja possível escrever programas corretos mesmo com uma estrutura de estado subótima, existem alguns princípios que podem orientá-lo a fazer escolhas melhores:

  1. Agrupe estados relacionados. Se você sempre atualiza duas ou mais variáveis de estado ao mesmo tempo, considere uni-las em uma única variável de estado.
  2. Evite contradições no estado. Quando o estado é estruturado de forma que várias partes do estado possam se contradizer e “discordar” umas das outras, você deixa espaço para erros. Tente evitar isso.
  3. Evite estados redundantes. Se você puder calcular algumas informações das props do componente ou de suas variáveis de estado existentes durante a renderização, não coloque essas informações no estado desse componente.
  4. Evite duplicação no estado. Quando os mesmos dados são duplicados entre várias variáveis de estado, ou dentro de objetos aninhados, é difícil mantê-los sincronizados. Reduza a duplicação quando puder.
  5. Evite estados muito aninhados. Um estado muito hierárquico não é muito conveniente para atualizar. Quando possível, prefira estruturar o estado de forma plana.

O objetivo por trás destes princípios é tornar o estado fácil de atualizar sem introduzir erros. Remover dados redundantes e duplicados do estado ajuda a garantir que todas as suas partes permaneçam sincronizadas. Isso é semelhante a como um engenheiro de banco de dados pode querer “normalizar” a estrutura do banco de dados para reduzir a chance de erros. Parafraseando Albert Einstein, “Faça seu estado o mais simples possível - mas não simples demais.”

Agora vamos ver como estes princípios se aplicam na prática.

As vezes você pode ficar em dúvida entre usar uma única variável de estado, ou várias.

Você deveria fazer isto?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

Ou isto?

const [position, setPosition] = useState({ x: 0, y: 0 });

Tecnicamente, você pode usar qualquer uma dessas abordagens. Mas se duas variáveis de estado sempre mudam juntas, pode ser uma boa ideia uní-las em uma única variável de estado. Assim você não esquecerá de sempre mantê-las sincronizadas, como neste exemplo onde mover o cursor atualiza ambas as coordenadas do ponto vermelho:

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

Outro caso em que você agrupará dados em um objeto ou em um array é quando você não sabe quantas variáveis de estado vai precisar. Por exemplo, é útil quando você tem um formulário onde o usuário pode adicionar campos personalizados.

Pitfall

Se sua variável de estado é um objeto, lembre-se de que você não pode atualizar apenas um campo nele sem explicitamente copiar os outros campos. Por exemplo, você não pode fazer setPosition({ x: 100 }) no exemplo acima porque ele não teria a propriedade y! Em vez disso, se você quisesse definir apenas x, faria setPosition({ ...position, x: 100 }), ou dividiria em duas variáveis de estado e faria setX(100).

Evite contradições no estado

Aqui está um formulário de feedback do hotel com as variáveis de estado isSending e isSent:

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Obrigado pelo feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>Como foi sua estadia no Pônei Saltitante?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Enviar
      </button>
      {isSending && <p>Enviando...</p>}
    </form>
  )
}

// Simula o envio de uma mensagem.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Embora este código funcione, ele deixa a porta aberta para estados “impossíveis”. Por exemplo, se você esquecer de chamar setIsSent e setIsSending juntos, você pode acabar em uma situação onde tanto isSending quanto isSent são true ao mesmo tempo. Quão mais complexo for o seu componente, mais difícil será entender o que aconteceu.

Como isSending e isSent nunca devem ser true ao mesmo tempo, é melhor substituí-los por uma variável de estado status que pode assumir um de três estados válidos: 'typing' (inicial), 'sending' e 'sent':

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Obrigado pelo feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>Como foi sua estadia no Pônei Saltitante?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Enviar
      </button>
      {isSending && <p>Enviando...</p>}
    </form>
  );
}

// Simula o envio de uma mensagem.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Voce ainda pode declarar algumas constantes para legibilidade:

const isSending = status === 'sending';
const isSent = status === 'sent';

Mas elas não são variáveis de estado, então você não precisa se preocupar com elas ficando fora de sincronia uma com a outra.

Evite estados redundantes

Se você pode calcular algumas informações das props do componente ou de suas variáveis de estado existentes durante a renderização, você não deveria colocar essas informações no estado desse componente.

Por exemplo, neste formulário. Ele funciona, mas você consegue encontrar algum estado redundante nele?

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Vamos fazer seu check-in</h2>
      <label>
        Primeiro nome:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Sobrenome:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Seu ticket será emitido para: <b>{fullName}</b>
      </p>
    </>
  )
}

Este formulário tem três variáveis de estado: firstName, lastName e fullName. No entanto, fullName é redundante. Você sempre pode calcular fullName a partir de firstName e lastName durante a renderização, então remova-o do estado.

Você pode fazer desta forma:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Vamos fazer seu check-in</h2>
      <label>
        Primeiro nome:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Sobrenome:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Seu ticket será emitido para: <b>{fullName}</b>
      </p>
    </>
  );
}

Aqui, fullName não é uma variável de estado. Em vez disso, ela é calculada durante a renderização:

const fullName = firstName + ' ' + lastName;

Como resultado, os manipuladores de mudança não precisam fazer nada de especial para atualizá-lo. Quando você chama setFirstName ou setLastName, você dispara uma nova renderização, e então o próximo fullName será calculado a partir dos dados atualizados.

Deep Dive

Não espelhe props no estado

Um exemplo comum de estado redundante são códigos como este:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

Aqui, uma variável de estado color é inicializada com a prop messageColor. O problema é que se o componente pai passar um valor diferente para messageColor depois (por exemplo, 'red' ao invés de 'blue'), a variável de estado color não seria atualizada! O estado é inicializado apenas durante a primeira renderização.

É por isso que “espelhar” alguma prop em uma variável de estado pode levar a confusão. Em vez disso, use a prop messageColor diretamente no seu código. Se você quiser dar um nome mais curto para ela, use uma constante:

function Message({ messageColor }) {
const color = messageColor;

Desta forma, ela não ficará fora de sincronia com a prop passada pelo componente pai.

”Espelhar” props no estado só faz sentido quando você quer ignorar todas as atualizações para uma prop específica. Por convenção, comece o nome da prop com initial ou default para deixar claro que seus novos valores são ignorados:

function Message({ initialColor }) {
// A variável de estado `color` guarda o *primeiro* valor de `initialColor`.
// Mudanças posteriores na *prop* `initialColor` são ignoradas.
const [color, setColor] = useState(initialColor); */

Evite duplicação no estado

Este componente de lista de menus permite que você escolha um único lanche de viagem dentre vários:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'alga crocante', id: 1 },
  { title: 'barra de granola', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>Qual o seu lanche de viagem?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Escolha</button>
          </li>
        ))}
      </ul>
      <p>Você selecionou {selectedItem.title}.</p>
    </>
  );
}

No momento, ele armazena o item selecionado como um objeto na variável de estado selectedItem. No entanto, isso não é bom: o conteúdo de selectedItem é o mesmo objeto que um dos itens dentro da lista items. Isso significa que as informações sobre o item em si estão duplicadas em dois lugares.

Por que isso é um problema? Vamos tornar cada item editável:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'alga crocante', id: 1 },
  { title: 'barra de granola', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>Qual o seu lanche de viagem?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Escolha</button>
          </li>
        ))}
      </ul>
      <p>Você selecionou {selectedItem.title}.</p>
    </>
  );
}

Observe como se você clicar primeiro em “Escolha” em um item e depois editá-lo, a entrada é atualizada, mas o rótulo na parte inferior não reflete as edições. Isso ocorre porque você duplicou o estado e esqueceu de atualizar selectedItem.

Embora você pudesse atualizar selectedItem também, uma correção mais fácil é remover a duplicação. Neste exemplo, em vez de um objeto selectedItem (que cria uma duplicação com objetos dentro de items), você mantém o selectedId no estado e depois obtém o selectedItem pesquisando o array items por um item com esse ID:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'alga crocante', id: 1 },
  { title: 'barra de granola', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>Qual o seu lanche de viagem?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Escolha</button>
          </li>
        ))}
      </ul>
      <p>Você selecionou {selectedItem.title}.</p>
    </>
  );
}

O estado costumava ser duplicado assim:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

Mas depois da mudança, é assim:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

A duplicação desapareceu, e você mantém apenas o estado essencial!

Agora, se você editar o item selecionado, a mensagem abaixo será atualizada imediatamente. Isso ocorre porque setItems dispara uma nova renderização, e items.find(...) encontraria o item com o título atualizado. Você não precisava manter o item selecionado no estado, porque apenas o ID selecionado é essencial. O resto poderia ser calculado durante a renderização.

Evite estados muito aninhados

Imagine um plano de viagem consistindo de planetas, continentes e países. Você pode ser tentado estruturar seu estado usando objetos e arrays aninhados, como neste exemplo:

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Terra',
    childPlaces: [{
      id: 2,
      title: 'África',
      childPlaces: [{
        id: 3,
        title: 'Botsuana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egito',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kênia',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Marrocos',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigéria',
        childPlaces: []
      }, {
        id: 9,
        title: 'África do Sul',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Ámericas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brasil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canadá',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'México',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trindade e Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Ásia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'Índia',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapura',
        childPlaces: []
      }, {
        id: 23,
        title: 'Coreia do Sul',
        childPlaces: []
      }, {
        id: 24,
        title: 'Tailândia',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnã',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europa',
      childPlaces: [{
        id: 27,
        title: 'Croácia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'França',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Alemanha',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Itália',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Espanha',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turquia',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Austrália',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (Polinésia Francesa)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Ilha da Páscoa (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (EUA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'Nova Zelândia',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Lua',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Marte',
    childPlaces: [{
      id: 47,
      title: 'Cidade do Milho',
      childPlaces: []
    }, {
      id: 48,
      title: 'Monte Verde',
      childPlaces: []      
    }]
  }]
};

Agora, digamos que você queira adicionar um botão para excluir um lugar que você já visitou. Como você faria isso? Atualizar estados aninhados envolve fazer cópias de objetos desde a parte que mudou. Excluir um lugar profundamente aninhado envolveria copiar toda a cadeia de lugares pai. Esse código pode ser muito verboso.

Se o estado for muito aninhado para ser atualizado facilmente, considere torná-lo “plano”. Aqui está uma maneira de você reestruturar esses dados. Em vez de uma estrutura em forma de árvore em que cada place tem um array de seus lugares filhos, você pode fazer com que cada lugar mantenha um array de IDs dos seus lugares filhos. Em seguida, armazene um mapeamento de cada ID de lugar para o lugar correspondente.

Essa reestruturação de dados pode lembrá-lo de ver uma tabela de banco de dados:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Terra',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'África',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botsuana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egito',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kênia',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Marrocos',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigéria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'África do Sul',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Américas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brasil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canadá',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'México',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trindade e Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Ásia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'Índia',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapura',
    childIds: []
  },
  23: {
    id: 23,
    title: 'Coreia do Sul',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Tailândia',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnã',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europa',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croácia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'França',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Alemanha',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Itália',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Espanha',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turquia',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Austrália',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (Polinésia Francesa)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Ilha de Páscoa (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 39,
    title: 'Hawaii (EUA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'Nova Zelândia',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Lua',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Marte',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Cidade do Milho',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Monte Verde',
    childIds: []
  }
};

Agora que o estado está “plano” (também conhecido como “normalizado”), atualizar itens aninhados fica mais fácil.

Para remover um lugar agora, você só precisa atualizar dois níveis de estado:

  • A versão atualizada de seu lugar pai deve excluir o ID removido de seu array childIds.
  • A versão atualizada do objeto “tabela” raiz deve incluir a versão atualizada do lugar pai.

Aqui está um exemplo de como você poderia fazer isso:

import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Cria uma nova versão do lugar pai
    // que não inclui o ID deste filho.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Atualiza o objeto de estado raiz...
    setPlan({
      ...plan,
      // ...para que tenha o pai atualizado.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Lugares para visitar</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Completar
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Você pode aninhar o estado o quanto quiser, mas torná-lo “plano” pode resolver inúmeros problemas. Isso torna o estado mais fácil de atualizar, e ajuda a garantir que você não tenha duplicação em diferentes partes de um objeto aninhado.

Deep Dive

Otimizando o uso da memória

Idealmente, você também removeria os itens excluídos (e seus filhos!) do objeto “tabela” para melhorar o uso da memória. Esta versão faz isso. Ele também usa Immer para tornar a lógica de atualização mais concisa.

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Por vezes, você também pode reduzir o aninhamento do estado movendo parte do estado aninhado para os componentes filhos. Isso funciona bem para o estado de UI efêmero que não precisa ser armazenado, como saber se o mouse está passando sobre um item.

Recap

  • Se duas variáveis de estado sempre são atualizadas juntas, considere uní-las em uma.
  • Escolha suas variáveis de estado cuidadosamente para evitar criar estados “impossíveis”.
  • Estruture seu estado de uma maneira que reduza as chances de você cometer um erro ao atualizá-lo.
  • Evite estados redundantes e duplicados para que você não precise mantê-los sincronizados.
  • Não coloque props dentro de estados a menos que você queira especificamente impedir atualizações.
  • Para padrões de UI como seleção, mantenha o ID ou o índice no estado em vez do objeto em si.
  • Se atualizar o estado profundamente aninhado for complicado, tente achatá-lo.

Challenge 1 of 4:
Corrija um componente que não está sendo atualizado

Este componente Clock recebe duas props: color e time. Quando você seleciona uma cor diferente na caixa de seleção, o componente Clock recebe uma prop color diferente de seu componente pai. No entanto, por algum motivo, a cor exibida não é atualizada. Por quê? Corrija o problema.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}