Tilarakenteen päättäminen

Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state.

Tulet oppimaan

  • Milloin käyttää yhtä vs useita tilamuuttujia
  • Mitä välttää kun tilaa järjestetään
  • Miten korjata yleisiä ongelmia tilarakenteessa

Tilarakenteen periaatteet

Kun kirjoitat komponenttia, joka ylläpitää jotain tilaa, sinun täytyy tehdä valintoja siitä miten monta tilamuuttujaa käytät ja missä muodossa niiden tietojen tulisi olla. Vaikka on mahdollista kirjoittaa oikeita ohjelmia epäoptimaalisella tilarakenteella, on muutamia periaatteita, jotka ohjaavat tekemään parempia valintoja:

  1. Ryhmitä toisiin liittyvä tila. Jos aina päivität kahta tai useampaa tilamuuttujaa yhdessä samanaikaisesti, harkitse näiden yhdistämistä yhteen tilamuuttujaan.
  2. Vältä tilan ristiriitoja. Kun tila on rakennettu tavalla, jossa useat tilan palaset saattavat olla ristiriidassa ja “olla eri mieltä” toistensa kanssa, jätät tilaa virheille. Koita välttää tätä.
  3. Vältä tarpeetonta tilaa. Jos voit laskea jotain tietoa komponentin propseista tai sen olemassa olevista tilamuuttujista renderöinnin aikana, sinun ei tulisi asettaa tätä tietoa komponentin tilaan.
  4. Vältä tilan toistoa. Kun jokin data on toistettuna useiden tilamuuttujien kesken, tai syvällä olioiden sisässä, on hankalaa pitää ne synkronoituna. Vähennä toistoa kun pystyt.
  5. Vältä syvään sisennettyä tilaa. Syvästi hierarkista tilaa ei ole kovin kätevä päivittää. Kun mahdollista, rakenna tila tasaisella tavalla.

Näiden periaatteiden taustalla oleva tavoite on tehdä tilasta helppoa päivittää tuottamatta virheitä. Tarpeettoman ja toistuvan datan poistaminen tilasta auttaa varmistamaan, että kaikki sen palaset pysyvät synkronoituna. Tämä on samankaltaista kuin se, miten tietokantasuunnittelija saattaa haluta “normalisoida” tietokannan rakenteen vähentääkseen bugien mahdollisuuksia. Albert Einsteinia lainatakseen, “Tee tilastasi niin yksinkertaista kuin mahdollista—mutta ei yksinkertaisempaa.”

Katsotaan nyt miten nämä periaatteet toimivat käytännössä.

Saatat joskus olla epävarma yhden vai useamman tilamuuttujan käytöstä.

Pitäisikö tehdä näin?

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

Vai näin?

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

Teknisesti ottaen, voit käyttää kumpaa tahansa tapaa. Mutta jos aina päivität kaksi tai useampaa tilamuuttujaa samanaikaisesti, kannattaa yhdistää ne yhteen tilamuuttujaan. Siten et unohda pitää niitä synkronoituna, kuten tässä esimerkissä, jossa kursorin liikuttaminen päivittää molempia punaisen pisteen koordinaatteja:

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

Toinen tilanne, jossa ryhmität tietoa olioon tai taulukkoon tapahtuu kun et tiedä miten monta eri tilan palasta tarvitset. Esimerkiksi, se käy hyödylliseksi kun tarvitset lomakkeen missä käyttäjä voi lisätä omia kenttiä.

Sudenkuoppa

Jos tilamuuttujasi on olio, muista, että et voi päivittää vain yhtä kenttää sen sisällä kopioimatta nimenomaisesti muita kenttiä. Esimerkiksi, et voi kutsua setPosition({ x: 100 }) yllä olevassa esimerkissä, koska siinä ei ole y kenttää ollenkaan! Sen sijaan, jos haluaisit päivittää vain x muuttujaa yksin, kutsuisit setPosition({ ...position, x: 100 }), tai jakaisit ne kahteen eri tilamuuttujaan ja kutsuisit setX(100).

Vältä tilan ristiriitoja

Tässä on hotellin palautelomake, jossa on isSending ja isSent tilamuuttujat:

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

Vaikka tämä koodi toimii, se jättää oven auki “mahdottomille” tiloille. Esimerkiksi, jos unohdat kutsua setIsSent ja setIsSending yhdessä, saatat joutua tilanteeseen, jossa isSending ja isSent ovat true samanaikaisesti. Mitä monimutkaisempi komponenttisi on, sitä hankalampi on ymmärtää mitä tapahtui.

Kerta isSending ja isSent eivät voi olla true samanaikaisesti, on parempi korvata ne yhdellä status tilamuuttujalla, joka hoitaan yhden kolmesta kelvollisesta tilast: 'typing' (oletus), 'sending', ja '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);
  });
}

Voit silti määritellä vakoita luettavuutta varten:

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

Mutta ne eivät ole tilamuuttujia, joten sinun ei tarvitse huolehtia niiden joutumista ristiriitaan.

Vältä tarpeetonta tilaa

Jos voit laskea jotain tilaa kompoenntin propseista tai sen olemassa olevista tilamuuttujista renderöinnin aikana, sinun ei tulisi laittaa tätä tietoa komponentin tilaan.

Esimerkiksi, katso tätä lomaketta. Se toimii, mutta pystytkö löytämään tarpeetonta tilaa siitä?

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

Lomakkeessa on kolme tilamuuttujaa: firstName, lastName, ja fullName. Kuitenkin, fullName on tarpeeton. Voit aina laskea fullName:n firstName ja lastName muuttujista renderöinnin aikana, joten poista se tilasta.

Tässä miten voit tehdä sen:

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

Tässä, fullName ei ole tilamuuttuja. Sen sijaan se lasketaan renderöinnin aikana:

const fullName = firstName + ' ' + lastName;

Näin ollen Tapahtumankäsittelijöiden ei tarvitse tehdä mitään erityistä sen päivittämiseksi. Kun kutsut setFirstnName tai setLastName funktioita, kutsut uudelleenrenderöinnin, ja seuraavan kerran fullName tullaan laskemaan uusiksi uuden datan pohjalta.

Syväsukellus

Älä peilaa propseja tilaan

Yleinen esimerkki tarpeettomasta tilasta on seuraavanlainen koodi:

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

Tässä color tilamuuttuja on alustettu messageColor propin perusteella. Ongelma on, jos pääkomponentti palauttaa uuden arvon messageColor propsille myöhemmin (esimerkiksi, 'red':n sijaan arvon 'blue'), color tilamuuttujaa ei tulla päivittämään! Tila alustetaan vain ensimmäisen renderöinnin aikana.

Tämän takia propsien “peilaaminen” tilamuuttujaan voi aiheuttaa hämmennystä. Sen sijaan käytä messageColor propsia suoraan koodissasi. Jos haluat antaa sille lyhyemmän nimen, käytä vakiota:

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

Näin se ei pääse epäsynkronoitumaan yläkomponentilta välitetyn propsin kanssa.

Propsien “peilaaminen” tilaan on järkevää vain kun haluat välttää tietyn propsin kaikki tilamuutokset. Aloita propsin nimi periaatteen mukaan sanoilla initial tai default selventääksesi, että sen uuden arvot jätetään huomioimatta:

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

Vältä tilan toistoa

Tämä menu listakomponentti antaa sinun valita yhden matkaherkun useista vaihtoehdoista:

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

Tällä hetkellä, se tallentaa valitun kohteen oliona selectedItem tilamuuttujaan: Kuitenkaan tämä ei ole hyvä: selectedItem muuttujan sisältö on sama olio kuin yksi items muuttujan sisällä. Tämä tarkoittaa, että tieto kohteesta itsestään on toistettuna kahteeen eri paikkaan.

Miksi tämä on ongelmallista? Tehdään kohteesta muokattava:

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

Huomaa miten jos ensiksi klikkaat “Choose” kohteesta ja sitten muokkaat sitä, syöttökenttä päivittyy, mutta alhaalla oleva teksti ei vastaa muutoksia. Tämä tapahtuu, koska sinulla on toistettua tilaa, ja unohdit päivittää selectedItem tilamuuttujaa.

Vaikka voisit päivittää selectedItem muuttujaa myöskin, helpompi tapa korjata tämä on poistamalla toisto. Tässä esimerkissä, selectedItem olion sijaan (joka luo toistoa items muuttujan sisällä olevista kohteista) pidät selectedId:n tilassa, ja sitten haet selectedItem:n hakemalla items taulusta kohteen, jolla on sama 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>
    </>
  );
}

(Vaihtoehtoisesti, voisit pitää valitun indeksin tilasa.)

Tila oli aiemmin monistettu näin:

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

Mutta muutosten jälkeen se on näin:

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

Toisto on nyt poistettu ja pidät vain tarpeelliset asiat tilassa!

Nyt jos haluat muokata valittua kohdetta, viesti alhaalla päivittyy välittömästi. Tämä tapahtuu koska setItems kutsuu uudelleenrenderöinnin ja items.find(...) hakee kohteen päivitetyllä otsikolla. Sinun ei täytynyt pitää valittua kohdetta tilassa, koska vain valitun kohteen ID on tarpeellinen. Loput voidaan laskea renderöinnin aikana.

Vältä syvään sisennettyä tilaa

Kuvittele matkasuunnitelma, joka koostuu planeetoista, maanosista ja maista. Saatat haluta jäsentää tilan käyttämällä sisäkkäisiä objekteja ja taulukkoja, kuten tässä esimerkissä:

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: []      
    }]
  }]
};

Oletetaan, että haluat lisätä painikkeen, jolla voit poistaa paikan, jossa olet jo käynyt. Miten se onnistuisi? Sisäkkäisen olion päivittäminen kopioi objektit aina muuttuneesta osasta ylöspäin. Syvällä olevan kohteen poistaminen edellyttäisi sen koko vanhemmuusketjun kopioimista. Tällainen koodi voi olla hyvin laajamittaista.

Jos tila on liian sisäkkäistä helposti päivitettäväksi, harkitse sen muuttamista “tasaiseksi”. Tässä on yksi tapa, jolla voit järjestää tämän datan. Puumaisen rakenteen sijaan, jossa kullakin place:lla on taulukko sen alapaikoista, voit sen sijaan pitää hallussaan joukon alapaikkojen ID:itä. Tämän jälkeen voit tallentaa kunkin paikan ID:n vastaamaan tiettyä paikkaa

This data restructuring might remind you of seeing a database table:

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: []
  }
};

Nyt kun tila on “tasainen” (tunnetaan myös nimellä “normalisoitu”), sisennettyjen kohteiden päivittäminen on helpompaa.

Kun haluat poistaa paikan, sinun tarvitsee vain päivittää kaksi tasoa tilasta:

  • Päivitetyn version pääkohteesta tulisi poistaa poistettu ID sen childIds taulukosta.
  • Päivitetyn version juuri “taulukko” olio tulisi päivittää sisällyttämään päivitetty versio sen pääkohteesta.

Tässä on esimerkki siitä, miten voisit toimia:

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

Voit sisentää tilaa niin paljon kuin haluat, mutta sen muuttaminen “tasaiseksi” voi ratkaista useita ongelmia. Se tekee tilan päivittämisestä helpompaa ja auttaa varmistamaan, että tilassa ei ole toistoa eri tasojen välillä.

Syväsukellus

Muistinkäytön parantaminen

Ihannetapauksessa poistaisit myös poistetut kohteet (ja niiden lapset!) “taulukko” oliosta muistin parantamiseksi. Tämä versio tekee myös tämän. Se myös käyttää Immeriä tehdäkseen tilan päivityslogiikan tiiviimmäksi.

import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, updatePlan] = useImmer(initialTravelPlan);

  function handleComplete(parentId, childId) {
    updatePlan(draft => {
      // Remove from the parent place's child IDs.
      const parent = draft[parentId];
      parent.childIds = parent.childIds
        .filter(id => id !== childId);

      // Forget this place and all its subtree.
      deleteAllChildren(childId);
      function deleteAllChildren(id) {
        const place = draft[id];
        place.childIds.forEach(deleteAllChildren);
        delete draft[id];
      }
    });
  }

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

Joskus voit myös vähentää tilan sisennystä siirtämällä tilan osia sen alakomponentteihin. Tämä toimii hyvin lyhytaikaiseen käyttöliittymätilaan, jota ei tarvitse tallentaa, kuten jos kursori on kohteen päällä.

Kertaus

  • Jos kaksi tilamuuttujaa päivittyvät yhdessä, harkitse niiden yhdistämistä yhdeksi.
  • Valitse tilamuuttujasi huolellisesti välttääksesi “mahdottomia” tiloja.
  • Järjestä tilasi tavalla, joka vähentää mahdollisuuksiasi tehdä ongelmia sitä päivittäessä.
  • Vältä toistuvaa ja turhaa tilaa, jotta sitä ei tarvitse pitää synkronoituna.
  • Älä välitä propseja suoraan tilaan, ellet nimenomaan halua välttää päivityksiä.
  • Käyttöliittymämalleihin kuten valintoihin, pidä ID tai indeksi tilassa koko kohteen olion sijaan.
  • Jos syvään sisäkkäisen olion päivittäminen on sekavaa, kokeile sen tasaamista.

Haaste 1 / 4:
Korjaa komponentti, joka ei päivity

Tämä Clock komponentti vastaanottaa kaksi propsia: color ja time. Kun valitset eri värin valintaruudusta, Clock komponentti vastaanottaa eri color propsin sen pääkomponentilta. Kuitenkin jostain syystä näytetty väri ei päivity. Miksi? Korjaa ongelma.

import { useState } from 'react';

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