Scegliere la struttura dello state

Strutturare bene lo state può fare la differenza tra un componente piacevole da modificare e debuggare, e uno che sia una costante fonte di bugs. Qui troverai alcuni consigli che dovresti considerare quando strutturi il tuo state

Imparerai

  • Quando usare una singola vs molteplici variabili di state
  • Cosa evitare quando organizzi lo state
  • Come correggere problemi comuni strutturando lo state

Principi fondamentali per strutturare lo state

Quando scrivi un componente che contiene uno state, dovrai fare delle scelte riguardo a quante variabili di state usare e con quali strutture dati lavorare. Benché sia possibile scrivere programmi funzionanti anche con uno state strutturato in maniera suboptimale, ci sono alcuni principi che ti possono guidare nel fare scelte migliori:

  1. Accorpare state in relazione. Se aggiorni frequentemente due o più variabili di state allo stesso tempo, considera di accorparle all’interno di una singola variabile di state.
  2. Evita contraddizioni nello state. Quando lo state è strutturato in modo tale che diversi pezzi dello state si possano contraddire e “essere in disaccordo” l’uno con l’altro, lasci spazio a errori. Cerca di evitarlo.
  3. Evita state ridondante. Se puoi calcolare alcune informazioni dalle props del componente o dalle sue variabili di state esistenti durante il rendering, non dovresti mettere quelle informazioni nello state del componente.
  4. Evita duplicazioni nello state. Reduce duplication when you can. Quando lo stesso dato è duplicato tra diverse variabili di state, o all’interno di oggetti nidificati, è difficile tenerli sincronizzati. Riduci la duplicazione quando puoi.
  5. Evita state profondamente nidificati Uno state troppo nidificato non è conveniente da aggiornare.

L’obbiettivo di questi principi è di rendere lo state facile da aggiornare senza introdurre errori. Rimuovere la ridondanza e la duplicazione del dato dallo state aiuta ad essere certi che tutti i suoi pezzi rimangano sincronizzati. Questo è simile a come un database engineer vorrebbe “normalizzare” la struttura del database per ridurre la probabilità di introdurre bugs. Parafrasando Albert Einstein, “Rendi il tuo state tanto semplice quanto può esserlo—ma non più semplice” Ora vediami questi principi all’azione.

A volte potresti essere incerto tra usaro una singola o molteplici variabili di state.

Dovresti fare cosi?

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

O cosi?

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

Tecnicamente, puoi usare entrambi gli approcci. Ma se due variabili dello state cambiano sempre insieme, potrebbe essere una buona idea accorparle in una singola variabile di state. In questo modo non dimenticherai di tenerle sempre sincronizzate, come in questo esempio dove muovere il cursore aggiorna entrambe le coordinate del punto rosso:

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

Un altro caso in qui accorperai dati in un oggetti o in un array è quando non sai di quanti pezzi di state avrai bisogno. Per esempio, è utile quando hai un form dove l’utente può aggiungere campi personalizzati.

Insidia

Se la tua variabile di state è un oggetto, ricorda che non puoi aggiornarne solo un campo senza esplicitamente copiare gli altri campi. Per esempio, non puoi fare setPosition({ x: 100 }) nell’esempio sopra perché non avrebbe la proprietà y! Invece, se vuoi impostare solamente x, dovresti fare setPosition({ ...position, x: 100 }), or dividendoli in due variabili di state e fare setX(100).

Evita contraddizioni nello state

Here is a hotel feedback form with isSending and isSent state variables:

Questo è il feedback form di un hotel con le variabili di state 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>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Anche se questo codice funziona, lascia spazio a state “impossibili”. Per esempio, se dimentichi di chiamare setIsSent e setIsSending insieme, potresti incappare in una situazione dove entrambi isSending e isSent sono true allo stesso tempo. Più complesso è il tuo componente, più difficile è capire cos’è successo,

Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable that may take one of three valid states: 'typing' (initial), 'sending', and 'sent': Poiché isSending e isSent non dovrebbero mai essere true allo stesso tempo, è meglio rimpiazzarli con una singola variabile di state status che possa prendere uno dei tre state validi: 'typing' (iniziale), '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>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Puoi anche dichiarare alcune costanti per aumentare la leggibilità:

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

Ma non sono variabili dello state, quindi non devi preoccuparti di tenerle sincronizzate.

Evita state ridondante

Se puoi calcolare alcune informazioni dalle props del componente o dalle sue variabili di state esistenti durante il rendering, non dovresti mettere quell’informazione nello state del componente.

Ad esempio, prendi questo form. Funziona, ma riesci a trovare qualche state ridondante?

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>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Questo form ha tre variabili nel suo state: firstName, lastName, e fullName. Ma fullName è ridondante. Puoi sempre calcolare fullName da firstName e lastName durante il render, quindi rimuovilo dallo state.

Questo è come puoi farlo:

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>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Qui, fullName non è una variabile dello state, è calcolata durante il render

const fullName = firstName + ' ' + lastName;

Come risultato, gli handlers non devono fare niente di speciale per aggiornarlo. Quando chiami setFirstName o setLastName, attivi un nuovo rendering, e quindi il nuovo fullName sarà calcolato a partire dai dati aggiornati.

Approfondimento

Non specchiare le props nello state

Un esempio comune di state ridondante è:

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

Qui la variabile di state color è inizializzata con la prop messageColor. Il problema è che *se il componente genitore passasse, in un secondo momento, un valore diverso di messageColor(per esempio, 'red' invece di 'blue'), la variabile di state color non verrebbe aggiornata! Lo state è inzializzato solo durante il primo render.

Questo è il motivo per cui “specchiare” alcune prop in una variabile di state può portare a confusione. Invece, usa la prop messageColor direttamente nel tuo codice. Se vuoi darle un nome più corto usa una costante:

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

In questo modo si manterrà sincronizzata con la prop passata dal componente genitore.

”Specchiare” le props nello state ha senso solo quando vuoi ignorare tutti gli aggiornamenti di una specifica prop. Per convenzione, inizia il nome della prop con initial o default per chiarire che i suoi nuovi valore sono ignorati:

function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);

Evita duplicazioni nello state

Questo componente menu ti permette di scegliere un singolo snack tra quelli disponibili:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

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

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Attualmente, memorizza l’item selezionato come un oggetto nell variabile di state selectedItem. Non è ideale: items contiene un oggetto tra quelli contenuti nella lista di selectedItem. Questo significa che l’informazione riguardante l’item stesso è duplicata in due posti.

Perché è un problema? Rendiamo ogni item editabile:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', 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>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Hai notato che se clicchi “Choose” su un item a poi lo modifichi, l’input si aggiorna ma la label in fondo non riflette le modifiche. Questo perché hai lo state duplicato, e hai dimenticato di aggiornare selectedItem.

Sebbene tu possa aggiornare anche selectedItem, rimuovere la duplicazione è un fix più semplice. In questo esempio, invece di un oggetto selectedItem (che crea una duplicazione con gli oggetti dentro items), tieni il selectedId nello state, e poi prendi il selectedItem cercando nell’array items l’item con quell’ID:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', 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>What's your travel snack?</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);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

Lo state era duplicato in questo modo:

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

Ma dopo le modifiche è cosi:

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

La duplicazione è stata rimossa, e mantieni solo lo state essenziale!

Ora se modifichi il selected item, il messaggio sotto si aggiornerà immediatamente. Questo perché setItems aziona un nuovo render, e items.find(...) trova l’item con il titolo aggiornato. Non avevi bisogno ti tenere l’item selezionato nello state, perché solo il selected ID è essenziale. Il resto può essere calcolato durante il render.

Evita state profondamente nidificati

Immagina un piano di viaggio che comprende pianeti, continenti e paesi. Potresti essere tentato di strutturare il suo state usando oggetti e array nidificati, come in questo esempio:

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

Ora diciamo che tu voglia aggiungere un bottone per cancellare un luogo che hai già visitato. Come procederesti? Aggiornare state nidificati implica fare copie degli oggetti fino alla parte che è cambiata. Cancellare un luogo che si trova in una posizione profonda implicherebbe copiare la sua intera catena di genitori. Un codice del genere può essere molto verboso.

Se lo state è troppo nidificato per essere aggiornato facilmente, considera di renderlo “flat”. Qui trovi un modo per ristrutturare questi dati. Invece di una struttura ad albero dove ogni place ha un array di suoi place figli, puoi fare in modo che ogni place contenga un array di IDs di suoi place figli. Poi salvare un mapping che metta in correlazione ogni place ID al suo place corrispondente.

Questa ristrutturazione dei dati potrebbe ricordarti la tabella di un database:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    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: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

Ora che lo state è “piatto” (anche detto “normalizzato”), aggiornare gli elementi nidificati diventa più facile.

Adesso per rimuovere un place devi solo aggiornare due livelli dello state:

  • La versione aggiornata del place genitore dovrebbe escludere l’ID rimosso dal suo array childIds.
  • La versione aggiornata dell’oggetto root dovrebbe includere la versione aggiornata del place genitore.

Qui un esempio di come potresti farlo:

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];
    // Create a new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Update the root state object...
    setPlan({
      ...plan,
      // ...so that it has the updated parent.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</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);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Non c’è limite a quanto profondamente puoi annidare lo state, ma farlo “flat” può risolvere numerosi problemi. Rende lo state più facile da aggiornare, e ti aiuta ad assicurarti di non avere duplicazioni nelle diverse parti di un oggetto nidificato.

Approfondimento

Migliora l’utilizzo della memoria

Idealmente, potresti rimuovere gli items eliminati (e il loro figli!) dall’oggetto “table” per migliorare l’utilizzo della memoria. Questa versione lo fa. Inoltre usa Immer per rendere la logica di aggiornamento dello state più 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": {}
}

Talvolta potrai anche ridurre la nidificazione dello state spostando parte dello state nidificato nei componenti figlio. Questo funziona bene per gli state effimeri dell’interfaccia utente che non hanno bisogno di essere memorizzati. Come ad esempio se un elemento è stato selezionato con il mouse.

Riepilogo

  • Se due variabili di state vengono sempre aggiornate insieme, valuta la possibilità di unirle in una sola.
  • Scegli con attenzione le variabili di state per evitare di creare stati “impossibili”.
  • Struttura lo state in modo da ridurre le possibilità di commettere errori durante l’aggiornamento.
  • Evita state ridondanti e duplicati in modo da non doverli mantenere sincronizzati.
  • Non inserire props nello state a meno che non si voglia specificatamente impedire gli aggiornamenti.
  • Nei pattern di interfaccia utente come la selezione, mantieni l’ID o l’indice nello state invece dell’oggetto stesso.
  • Se l’aggiornamento di uno state profondamente annidato è complicato, prova ad appiattirlo.

Sfida 1 di 4:
Correggi un componente che non si sta aggiornando

Questo componente Clock riceve due proprietà: color e time. Quando selezioni un colore diverso nella casella di selezione, il componente Clock riceve una proprietà color diversa dal suo componente padre. Tuttavia, per qualche motivo, il colore visualizzato non viene aggiornato. Perché? Risolvi il problema.

import { useState } from 'react';

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