Taulukkojen päivittäminen tilassa

Taulukot ovat mutatoitavissa, mutta niitä tulisi käsitellä kuin ne olisivat ei-mutatoitavissa kun tallennat niitä tilaan. Kuten olioiden kanssa, päivitä tilaan tallennettu taulukko luomalla uusi (tai tekemällä kopio vanhasta) ja sitten aseta tila käyttämään uutta taulukkoa.

Tulet oppimaan

  • Miten lisätä, poistaa, tai muuttaa taulukon kohteita Reactin tilassa
  • Miten päivittää taulukon sisällä olevaa oliota
  • Miten teet taulukoiden kopioimisesta vähemmän toistuvaa Immerillä

Taulukkojen päivittäminen ilman mutaatiota

JavaScriptissa taulukot ovat kuin toisenlainen olio. Kuten olioiden kanssa, sinun tulisi käsitellä Reactin tilan taulukkoja vain-luku muodossa. Tämä tarkoittaa, että sinun ei pitäisi uudelleen määritellä taulukon kohteita arr[0] = 'bird' tavalla eikä kannattaisi käyttää mutatoivia tapoja muokata taulukkoa, kuten push() ja pop().

Sen sijaan, joka kerta kun haluat päivittää taulukkoa, haluat välittää uuden taulukon tilan asettajafunktiolle. Voit tehdä tämän luomalla uuden taulukon alkuperäisestä taulukosta kutsumalla sen ei-mutatoivia metodeja kuten filter() ja map(). Sitten voit asettaa uuden taulukon tilaksi.

Tässä on viitetaulukko yleisistä taulukon toiminnoista. Kun käsittelet taulukoita Reactin tilassa, sinun pitäisi välttää metodeja taulukon vasemmalla sarakkeella, ja sen sijaan suosia metodeja taulukon oikealla sarakkeella:

vältä (mutatoi taulukkoa)suosi (palauttaa uuden taulukon)
lisääminenpush, unshiftconcat, [...arr] spread syntaksi (esimerkki)
poistaminenpop, shift, splicefilter, slice (esimerkki)
korvaaminensplice, arr[i] = ... määrittelymap (esimerkki)
järestäminenreverse, sortkopioi taulukko ensin (esimerkki)

Vaihtoehtoisesti, voit käyttää Immeriä, jonka avulla voit käyttää metodeja molemmista sarakkeista.

Sudenkuoppa

Unfortunately, slice and splice are named similarly but are very different:

  • slice lets you copy an array or a part of it.
  • splice mutates the array (to insert or delete items).

In React, you will be using slice (no p!) a lot more often because you don’t want to mutate objects or arrays in state. Updating Objects explains what mutation is and why it’s not recommended for state.

Taulukkoon lisääminen

push() funktio tekee mutaation, jota et halua:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Sen sijaan luo uusi taulukko, joka sisältää aiemman taulukonkohteet ja uuden kohteen taulukon lopussa. Tämän toteuttamiseen on useita tapoja, mutta helpoin on käyttää ... array spread syntaksia:

setArtists( // Korvaa tila
[ // uudella taulukolla
...artists, // joka sisältää vanhat kohteet
{ id: nextId++, name: name } // ja yhden uuden lopussa
]
);

Nyt se toimii oikein:

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Taulukon spread syntaksilla voit myös lisätä kohteen taulukon alkuun, sijoittamalla sen ennen alkuperäistä ...artists taulukkoa:

setArtists([
{ id: nextId++, name: name },
...artists // Aseta vanhat kohteet loppuun
]);

Näin spread syntaksi hoitaa sekä push() funktion että unshift() funktion työt. Kokeile yllä olevassa hiekkalaatikossa!

Taulukosta poistaminen

Helpoin tapa poistaa kohde taulukosta on suodattamalla se pois. Toisin sanoen, luot uuden taulukon, joka ei sisällä poistettavaa kohdetta. Voit tehdä tämän käyttämällä filtermetodia, esimerkiksi:

import { useState } from 'react';

let initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [artists, setArtists] = useState(
    initialArtists
  );

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Klikkaa “Delete” painiketta muutaman kerran ja tarkastele sen klikkauskäsittelijää.

setArtists(
artists.filter(a => a.id !== artist.id)
);

Tässä artists.filter(a => a.id !== artist.id) tarkoittaa “luo uusi taulukko, joka koostuu artists kohteista, joiden ID:t ovat eri kuin artist.id”. Toisin sanoen, jokaisen artistin “Delete” painike suodattaa juuri sen artistin pois taulukosta, ja sitten pyytävät uudelleen renderöintiä lopullisella taulukolla. Huomaa, että filter ei muokkaa olemassa olevaa taulukkoa.

Taulukon muuntaminen

Jos haluat muuntaa joitakin tai kaikkia taulukon kohteita, voit käyttää map() metodia luodaksesi uuden taulukon. Funktion, jonka välität map metodille voi määritellä mitä teet kullekin kohteelle sen datan tai indeksin (tai molempien) pohjalta.

Tässä esimerkissä taulukko sisältää koordinaatit kahdelle ympyrälle ja yhdelle neliölle. Kun painat painiketta, se siirtää vain ympyröitä 50 pikseliä alaspäin. Se tekee tämän luomalla uuden taulukon käyttäen map() funktiota:

import { useState } from 'react';

let initialShapes = [
  { id: 0, type: 'circle', x: 50, y: 100 },
  { id: 1, type: 'square', x: 150, y: 100 },
  { id: 2, type: 'circle', x: 250, y: 100 },
];

export default function ShapeEditor() {
  const [shapes, setShapes] = useState(
    initialShapes
  );

  function handleClick() {
    const nextShapes = shapes.map(shape => {
      if (shape.type === 'square') {
        // No change
        return shape;
      } else {
        // Return a new circle 50px below
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Re-render with the new array
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Move circles down!
      </button>
      {shapes.map(shape => (
        <div
          key={shape.id}
          style={{
          background: 'purple',
          position: 'absolute',
          left: shape.x,
          top: shape.y,
          borderRadius:
            shape.type === 'circle'
              ? '50%' : '',
          width: 20,
          height: 20,
        }} />
      ))}
    </>
  );
}

Taulukon kohteiden korvaaminen

On yleistä, että haluat korvata yhden tai useamman kohteen taulukossa. It is particularly common to want to replace one or more items in an array. Määritykset kuten arr[0] = 'bird' mutatoivat alkuperäistä taulukkoa, joten sen sijaan voit käyttää map metodia myös tähän.

Korvataksesi kohteen, luo uusi taulukko map:lla. map kutsun sisälle vastaanotat kohteen indeksin toisena argumenttina. Käytä sitä päättämään, palautetaanko alkuperäinen kohde (ensimmäinen argumentti) vai jotain muuta:

import { useState } from 'react';

let initialCounters = [
  0, 0, 0
];

export default function CounterList() {
  const [counters, setCounters] = useState(
    initialCounters
  );

  function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // Increment the clicked counter
        return c + 1;
      } else {
        // The rest haven't changed
        return c;
      }
    });
    setCounters(nextCounters);
  }

  return (
    <ul>
      {counters.map((counter, i) => (
        <li key={i}>
          {counter}
          <button onClick={() => {
            handleIncrementClick(i);
          }}>+1</button>
        </li>
      ))}
    </ul>
  );
}

Tiettyyn kohtaan lisääminen

Joskus saatat haluta sijoittaa kohteen tiettyyn kohtaan, joka ei kuitenkaan ole taulukon alussa tai lopussa. Voit tehdä tämän käyttämällä ... syntaksia yhdessä slice() metodin kanssa. slice() metodi antaa sinun leikata “palan” taulukosta. Sijoittaaksesi kohteen, luot uuden taulukon joka levittää “palan” ennen sijoituskohtaa, sitten uuden kohteen, ja lopuksi loput alkuperäisestä taulukosta.

Tässä esimerkissä, “Insert” painike sijoittaa aina indeksiin 1:

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Could be any index
    const nextArtists = [
      // Items before the insertion point:
      ...artists.slice(0, insertAt),
      // New item:
      { id: nextId++, name: name },
      // Items after the insertion point:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insert
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Muiden muutosten tekeminen taulukkoihin

On joitakin asioita, joita et voi tehdä pelkällä spread syntaksilla ja ei-mutatoivilla metodeilla kuten map():lla ta filter():lla. Esimerkiksi, saatat haluta kääntää taulukon järjestyksen tai suodattaa taulukkoa. JavaScriptin reverse()- ja sort()`-metodit muuttavat alkuperäistä taulukkoa, joten niitä ei voi käyttää suoraan.

Kuitenkin, voit kopioida taulukon ensiksi ja sitten tehdä muutoksia siihen.

Esimerkiksi:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        Reverse
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

Tässä käytät [...list] spread syntaksia luodaksesi kopion alkuperäisestä taulukosta ensiksi. Nyt kun kopio on luotu, voit käyttää mutatoivia metodeja kuten nextList.reverse() tai nextList.sort(), tai jopa määrittää yksittäisiä kohteita nextList[0] = "something" määrittelyllä.

Kuitenkin, vaikka kopioisit taulukon, et voi mutatoida sen sisällä olevia kohteita suoraan. Tämä siksi koska kopiointi on pinnallista—uusi taulukko sisältää samat kohteet kuin alkuperäinen. Joten jos muokkaat kopioidun taulukon sisällä olevia olioita, mutatoit olemassa olevaa tilaa. Esimerkiksi, alla oleva koodi on ongelmallista.

const nextList = [...list];
nextList[0].seen = true; // Ongelma: mutatoi list[0] kohdetta
setList(nextList);

Vaikka nextList ja list ovat kaksi eri taulukkoa, nextList[0] ja list[0] osoittavat samaan olioon. Joten muuttamalla nextlist[0].seen kohdetta muutat myös list[0].seen kohdetta. Tämä on tilanmuutos, jota tulisi välttää! Voit ratkaista ongelman samalla tavalla kuin sisäkkäisten olioiden päivittäminen—eli kopioimalla yksittäiset kohteet, joita haluat muuttaa mutatoinnin sijaan. Näin se onnistuu.

Olioiden päivittäminen taulukon sisällä

Oliot eivät oikeasti sijaitse taulukkojen “sisällä”. Ne saattavat näyttäytyä olevan “sisällä” koodissa, mutta jokainen olio taulukossa on erillinen arvo johon taulukko “osoittaa”. Tämän takia täytyy olla tarkkana kun muutat sisäkkäisiä kenttiä kuten list[0]. Toisen henkilön taideteoslista saattaa osoittaa samaan kohteeseen taulukossa!

Päivittäessä sisäkkäistä tilaa, sinun täytyy luoda kopioita siihen pisteeseen saakka mitä haluat päivittää, ja aina ylätasoon asti. Katsotaan miten tämä toimii.

Tässä esimerkissä, kahdella erillisellä taideteoslistalla on sama aloitustila. Niiden on tarkoitus olla eristettyinä, mutta mutaation seurauksena niiden tila on vahingossa jaettu. Valintaruudun valitseminen yhdessä listassa vaikuttaa toiseen listaan:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    const myNextList = [...myList];
    const artwork = myNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setMyList(myNextList);
  }

  function handleToggleYourList(artworkId, nextSeen) {
    const yourNextList = [...yourList];
    const artwork = yourNextList.find(
      a => a.id === artworkId
    );
    artwork.seen = nextSeen;
    setYourList(yourNextList);
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Ongelma on seuraavassa koodissa:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Ongelma: mutatoi olemassa olevaa kohdetta
setMyList(myNextList);

Vaikka myNextList taulukko on uusi, kohteet ovat samat kuin alkuperäisessä myList taulukossa. Joten artwork.seen:n muuttaminen muuttaa alkuperäistä taideteoskohdetta. Tuo taideteos on myös yourList taulukossa, joka aiheuttaa bugin. Tällaisia bugeja voi olla vaikea ajatella, mutta onneksi ne katoavat jos vältät tilan mutatointia.

Voit käyttää map metodia korvataksesi vanhan kohteen sen päivitetyllä versiolla ilman mutatointia.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Luo *uusi* olio muutoksilla
return { ...artwork, seen: nextSeen };
} else {
// Ei muutosta
return artwork;
}
}));

Tässä, ... on olion levityssyntaksi, jota käytetään uuden kopion luomiseksi.

With this approach, none of the existing state items are being mutated, and the bug is fixed:

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Yleisesti ottaen, sinun tulisi mutatoida oliota joita olet juuri luonut. Jos olet sijoittamassa uutta taideteosta, voisit mutatoida taulukkoa, mutta jos käsittelet jotain, joka on jo tilassa, sinun täytyy tehdä kopio.

Kirjoita tiivis päivityslogiikka Immerillä

Sisennettyjen taulukoiden päivittäminen ilman mutaatiota saattaa koitua toistuvaksi. Juuri kuten olioiden kanssa:

  • Yleisesti ottaen sinun ei tulisi päivittää tilaa kahta tasoa syvemmältä. Jos tilaoliosi ovat todella syviä, saatat haluta järjestää ne eri tavalla, jotta ne olisivat tasaisia.
  • Jos et haluat muuttaa tilasi rakennetta, saatat pitää Immer:stä, jonka avulla voit kirjoittaa kätevällä mutta mutatoivalla syntaksilla, hoitaen kopiot puolestasi.

Tässä on Art Bucket List esimerkki uudelleenkirjoitettuna Immerillä:

import { useState } from 'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, updateMyList] = useImmer(
    initialList
  );
  const [yourList, updateYourList] = useImmer(
    initialList
  );

  function handleToggleMyList(id, nextSeen) {
    updateMyList(draft => {
      const artwork = draft.find(a =>
        a.id === id
      );
      artwork.seen = nextSeen;
    });
  }

  function handleToggleYourList(artworkId, nextSeen) {
    updateYourList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Huomaa miten Immerillä mutatointi kuten artwork.seen = nextSeen on nyt sallittua:

updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});

Tämä siksi koska et mutatoi alkuperäistä tilaa, vaan mutatoit erityistä draft oliota, jonka Immer tarjoaa. Vastaavasti, voit käyttää mutatoivia metodeja kuten push() ja pop() draft olion sisällöille.

Konepellin alla Immer luo aina uuden tilan alusta pohjautuen muutoksiin, joita teit draft oliolle. Tämä pitää tapahtumakäsittelijäsi todella tiiviinä mutatoimatta tilaa.

Kertaus

  • Voit laittaa taulukkoja tilaan, mutta et voi muuttaa niitä.
  • Mutatoinnin sijaan luot uuden version siitä ja päivität tilan vastaamaan sitä.
  • Voit käyttää [...arr, newItem] array levityssyntaksia luodaksesi taulukon uusilla kohteilla.
  • Voit käyttää filter() ja map() metodeja luodaksesi uuden taulukon suodatetuilla tai muunneltuilla kohteilla.
  • Voit käyttää Immeriä pitääksesi koodin tiivinä.

Haaste 1 / 4:
Päivitä kohdetta ostoskorissa

Täytä handleIncreaseClick:n logiikka, jotta ”+“:n painaminen kasvattaa vastaavaa numeroa:

import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Cheese',
  count: 5,
}, {
  id: 2,
  name: 'Spaghetti',
  count: 2,
}];

export default function ShoppingCart() {
  const [
    products,
    setProducts
  ] = useState(initialProducts)

  function handleIncreaseClick(productId) {

  }

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          {' '}
          (<b>{product.count}</b>)
          <button onClick={() => {
            handleIncreaseClick(product.id);
          }}>
            +
          </button>
        </li>
      ))}
    </ul>
  );
}