Ažuriranje nizova u state-u

Nizovi su promenljivi u JavaScript-u, ali biste trebali da ih tretirate kao immutable kada ih čuvate u state-u. Kao i kod objekata, kada želite da ažurirate niz koji se nalazi u state-u, potrebno je da kreirate novi (ili napravite kopiju postojećeg), a zatim postavite state da koristi taj novi niz.

Naučićete:

  • Kako da dodajete, uklanjate ili menjate članove niza u React state-u
  • Kako da ažurirate objekat unutar niza
  • Kako učiniti da se kopiranje nizova manje ponavlja uz pomoć Immer-a

Ažuriranje nizova bez mutacije

U JavaScript-u, nizovi su samo još jedna vrsta objekata. Kao i sa objektima, trebali biste tretirati nizove u React state-u kao read-only. To znači da ne trebate dodeljivati vrednost članovima niza poput arr[0] = 'bird', a takođe ne biste trebali da koristite metode koje mutiraju niz, kao što su push() i pop().

Umesto toga, svaki put kada želite ažurirati niz, želećete da prosledite novi niz u vašu state setter funkciju. Da biste to uradili, možete kreirati novi niz na osnovu originalnog niza u state-u pozivanjem metoda koje ne vrše mutaciju poput filter() i map(). Onda možete postaviti state na novi rezultujući niz.

Ovde je referentna tabela uobičajenih operacija nad nizovima. Kada koristite nizove unutar React state-a, trebate izbegavati metode u levoj koloni i, umesto toga, koristiti metode iz desne kolone:

izbegavati (mutira niz)koristiti (vraća novi niz)
dodavanjepush, unshiftconcat, [...arr] spread sintaksa (primer)
uklanjanjepop, shift, splicefilter, slice (primer)
zamenasplice, arr[i] = ... dodelamap (primer)
sortiranjereverse, sortprvo kopirati niz (primer)

Alternativno, možete koristiti Immer koji vam dopušta da koristite metode iz obe kolone.

Pitfall

Nažalost, slice i splice se zovu slično, ali su veoma drugačiji:

  • slice kopira niz ili deo niza.
  • splice mutira niz (za ubacivanje ili uklanjanje članova).

U React-u, koristićete slice (ne p!) mnogo češće zato što ne želite da mutirate objekte i nizove u state-u. Ažuriranje objekata objašnjava šta je to mutacija i zašto nije preporučena za state.

Dodavanje u niz

push() ce mutirati niz, a to ne želite:

import { useState } from 'react';

let nextId = 0;

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

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

Umesto toga, napravite novi niz koji sadrži sve postojeće članove, ali i novi član na kraju. Postoji više načina da to uradite, ali najlakši je upotrebom ... spread sintakse za nizove:

setArtists( // Zameni state
[ // sa novim nizom
...artists, // koji sadrži sve stare članove
{ id: nextId++, name: name } // kao i novi član na kraju
]
);

Sada radi kako treba:

import { useState } from 'react';

let nextId = 0;

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

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

Spread sintaksa za nizove omogućava i dodavanje članova pre originalnih ...artists:

setArtists([
{ id: nextId++, name: name },
...artists // Stavi stare članove na kraj
]);

Na ovaj način, spread može obavljati oba posla: push() - dodavanje na kraj niza i unshift() - dodavanje na početak niza. Probajte u sandbox-u iznad!

Uklanjanje iz niza

Najlakši način za uklanjanje člana iz niza je da ga isfiltrirate. Drugim rečima, napravićete novi niz koji ne sadrži taj član. Da biste to uradili, koristite filter metodu, na primer:

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>Inspirativni skulptori:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Ukloni
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Kliknite dugme “Ukloni” par puta i pogledajte njegov klik handler.

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

Ovde, artists.filter(a => a.id !== artist.id) znači “napravi niz koji sadrži sve artists čiji ID je različit od artist.id”. Drugim rečima, “Ukloni” dugme kod svakog umetnika će isfiltrirati tog umetnika van niza, a onda zatražiti ponovni render sa rezultujućim nizom. Primetite da filter ne menja originalni niz.

Transformisanje niza

Ako želite da promenite neki ili sve članove niza, možete koristiti map() da kreirate novi niz. Funkcija koju prosledite u map može odlučiti šta uraditi za svaki član, na osnovu njegovih podataka ili indeksa (ili oba).

U ovom primeru, niz sadrži koordinate dva kruga i jednog kvadrata. Kada pritisnete dugme, samo će se krugovi pomeriti na dole za 50 piksela. To se radi pravljenjem novog niza upotrebom map() metode:

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') {
        // Nema promene
        return shape;
      } else {
        // Vrati novi krug 50px ispod
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Ponovni render sa novim nizom
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Pomeri krugove dole!
      </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,
        }} />
      ))}
    </>
  );
}

Zamena članova niza

Dosta je uobičajeno da želite zameniti jedan ili više članova niza. Dodele poput arr[0] = 'bird' mutiraju originalni niz, tako da ćete umesto toga želeti i ovde da koristite map.

Da biste zamenili član, napravite novi niz sa map. Unutar map poziva, dobićete indeks člana kao drugi argument. Iskoristite ga da odlučite da li da vratite originalni član (prvi argument) ili nešto drugo:

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) {
        // Inkrementiraj kliknuti brojač
        return c + 1;
      } else {
        // Ostatak se ne menja
        return c;
      }
    });
    setCounters(nextCounters);
  }

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

Ubacivanje u niz

Ponekad ćete želeti da ubacite član u niz na određenu poziciju koja nije ni početak ni kraj. Da biste to uradili, možete koristiti ... spread sintaksu za nizove zajedno sa slice() metodom. slice() metoda vam omogućava da uzmete “parče” niza. Da biste ubacili član, kreiraćete niz koji sadrži parče pre mesta ubacivanja, novi član i ostatak originalnog niza.

U ovom primeru, dugme “Ubaci” uvek ubacuje na indeks 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; // Može biti bilo koji indeks
    const nextArtists = [
      // Članovi pre mesta ubacivanja:
      ...artists.slice(0, insertAt),
      // New item:
      { id: nextId++, name: name },
      // Članovi nakon mesta ubacivanja:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

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

Pravljenje ostalih izmena nad nizom

Postoje stvari koje ne možete uraditi samo sa spread sintaksom i metodama koje ne vrše mutacije poput map() i filter(). Na primer, možete želeti da obrnete ili sortirate niz. JavaScript reverse() i sort() metode mutiraju originalni niz, pa ih ne možete koristiti direktno.

Međutim, možete prvo kopirati niz, pa onda praviti promene nad njim.

Na primer:

import { useState } from 'react';

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}>
        Obrni
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

Ovde prvo koristite [...list] spread sintaksu da napravite kopiju originalnog niza. Sad kad imate kopiju, možete koristiti metode koje mutiraju kao što su nextList.reverse() ili nextList.sort(), a možete i vršiti dodelu određenim članovima sa nextList[0] = "something".

Međutim, čak i ako kopirate niz, ne možete mutirati postojeće članove unutar njega direktno. Ovo je zbog toga što je kopija plitka—novi niz će sadržati iste članove kao i originalni. Ako izmenite objekat unutar kopiranog niza, vi mutirate postojeći state. Na primer, kod poput ovog je problematičan.

const nextList = [...list];
nextList[0].seen = true; // Problem: mutira list[0]
setList(nextList);

Iako su nextList i list dva različita niza, nextList[0] i list[0] pokazuju na isti objekat. To znači da promenom nextList[0].seen, takođe menjate i list[0].seen. Ovo je mutacija state-a, što trebate izbegavati! Ovaj problem možete rešiti na sličan način kao i ažuriranje ugnježdenih JavaScript objekata—umesto mutacije, možete kopirati pojedinačne članove koje želite da promenite. Evo i kako.

Ažuriranje objekata unutar nizova

Objekti se ne nalaze zapravo “unutar” nizova. U kodu može delovati kao da su “unutar”, ali svaki objekat u nizu je posebna vrednost na koju niz “pokazuje”. Zbog toga trebate biti oprezni kad menjate ugnježdena polja poput list[0]. Lista umetničkih dela druge osobe može pokazivati na isti element niza!

Kada ažurirate ugnježdeni state, trebate kreirati kopije od mesta gde želite ažurirati, pa nagore sve do najvišeg nivoa. Hajde da vidimo kako to funkcioniše.

U ovom primeru, dve različite liste umetničkih dela imaju isti inicijalni state. Trebalo bi da su izolovane, ali, zbog mutacije, slučajno dele state, pa štikliranje u jednoj listi utiče i na drugu:

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>Lista željenih umetnosti</h1>
      <h2>Moja lista:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Tvoja lista:</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>
  );
}

Problem je u ovom kodu:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Problem: mutira postojeći član
setMyList(myNextList);

Iako je sam myNextList niz novi, članovi u njemu su isti kao u originalnom myList nizu. Promenom artwork.seen menjate originalno umetničko delo. To umetničko delo je i u yourList, što prouzrokuje bug. Može biti teško razmišljati o ovakvim bug-ovima, ali, srećom, oni nestaju ako izbegavate mutiranje state-a.

Možete koristiti map da zamenite stari član sa njegovom ažuriranom verzijom bez mutacije.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Napravi *novi* objekat sa promenama
return { ...artwork, seen: nextSeen };
} else {
// Nema promena
return artwork;
}
}));

Ovde, ... je objektna spread sintaksa kojom se kreira kopija objekta.

Ovim pristupom se ne mutira nijedan član postojećeg state-a i popravlja se bug:

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) {
        // Napravi *novi* objekat sa promenama
        return { ...artwork, seen: nextSeen };
      } else {
        // Nema promena
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Napravi *novi* objekat sa promenama
        return { ...artwork, seen: nextSeen };
      } else {
        // Nema promena
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Lista željenih umetnosti</h1>
      <h2>Moja lista:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Tvoja lista:</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>
  );
}

Uglavnom, trebali biste mutirati samo objekte koje ste upravo kreirali. Ako ubacujete novo umetničko delo, možete ga mutirati, ali ako menjate nešto što je već u state-u, trebate napraviti kopiju.

Pisanje koncizne logike ažuriranja sa Immer-om

Ažuriranje ugnježdenih nizova bez mutacije može postati ponovljivo. Baš kao i sa objektima:

  • Generalno, ne biste trebali da ažurirate state za više od par nivoa dubine. Ako su vaši state objekti dosta hijerarhijski duboki, možete probati da ih drugačije strukturirate kako bi bili flat.
  • Ako ne želite promeniti strukturu vašeg state-a, možda biste voleli da koristite Immer koji omogućava pisanje zgodne sintakse za mutiranje i brine o pravljenju kopija za vas.

Ovde je primer Liste željenih umetnosti napisan pomoću Immer-a:

{
  "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": {}
}

Primetite kako je sa Immer-om, mutacija poput artwork.seen = nextSeen sada u redu:

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

To se dešava zato što ne mutirate originalni state, već poseban draft objekat koji Immer pruža. Slično tome, možete upotrebiti mutirajuće metode poput push() i pop() nad draft objektom.

Iza kulisa, Immer uvek konstruiše novi state od nule na osnovu promena koje ste izvršili nad draft-om. Ovo vaše event handler-e čini konciznijim bez mutiranja state-a.

Recap

  • Možete držati nizove u state-u, ali ih ne smete menjati.
  • Umesto da mutirate niz, napravite novu verziju niza i ažurirajte state da je koristi.
  • Možete koristiti [...arr, newItem] spread sintaksu za nizove da napravite nizove sa novim članovima.
  • Možete koristiti filter() i map() za kreiranje novih nizova sa filtriranim i transformisanim članovima.
  • Možete koristiti Immer da vam kod bude koncizniji.

Izazov 1 od 4:
Ažurirati proizvod u korpi za kupovinu

Popunite handleIncreaseClick metodu tako da pritiskanje ”+” dugmeta inkrementira odgovarajući broj:

import { useState } from 'react';

const initialProducts = [{
  id: 0,
  name: 'Baklava',
  count: 1,
}, {
  id: 1,
  name: 'Sir',
  count: 5,
}, {
  id: 2,
  name: 'Špagete',
  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>
  );
}