State İçerisindeki Dizileri Güncelleme

Diziler JavaScript’te değiştirilebilirdir, ancak bunları state içinde depolarken değiştirilemez olarak ele almalısınız. Tıpkı nesnelerde olduğu gibi, state’te depolanan bir diziyi güncellemek istediğinizde yeni bir dizi oluşturmanız (veya var olanın bir kopyasını oluşturmanız) ve ardından yeni oluşturduğunuz diziyi kullanmak için state’i güncellemeniz gerekir.

Bunları öğreneceksiniz

  • React state’indeki bir diziye öğeler nasıl eklenir, çıkarılır ya da değiştirilir
  • Dizi içindeki nesne nasıl güncellenir
  • Immer kullanarak dizi kopyalama işlemi daha az tekrarla nasıl yapılır

Dizileri değiştirmeden güncelleme

JavaScript’te diziler bir nesne türüdür. Nesnelerde olduğu gibi, React state’indeki dizileri salt okunur olarak görmelisiniz. Bu, arr[0] = 'bird' şeklinde bir dizi içindeki öğeleri başka değerlere yeniden atamamanız, ayrıca push() ve pop() gibi dizileri mutasyona uğratan JavaScript metodlarını kullanmamanız gerektiği anlamına gelir.

Bu metodları kullanmak yerine, bir diziyi her güncellemek istediğinizde state setter fonksiyonunuza yeni bir dizi iletmelisiniz. Bunu yapmak için, filter() ve map() gibi diziyi mutasyona uğratmayan JavaScript metodlarını kullanarak orijinal diziden yeni bir dizi oluşturabilirsiniz. Ardından, state’inizi kopyaladığınız dizi olarak güncelleyebilirsiniz.

Aşağıda sık kullanılan dizi metodları tablo halinde gösterilmiştir. React state’indeki dizilerle çalışırken sol sütundaki metodları kullanmaktan kaçınarak sağ sütundaki metodları tercih etmelisiniz.

kaçınılacaklar (diziyi mutasyona uğratır)tercih edilecekler (yeni bir dizi döndürür)
eklemekpush, unshiftconcat, [...arr] spread sözdizimi (örnek)
çıkartmakpop, shift, splicefilter, slice (örnek)
değiştirmeksplice, arr[i] = ... atamasımap (örnek)
sıralamakreverse, sortilk önce diziyi kopyalayın (örnek)

Alternatif olarak, her iki sütundaki metodları kullanmanıza izin veren [Immer’ı] (#write-concise-update-logic-with-immer) tercih edebilirsiniz.

Tuzak

Ne yazık ki, slice ve splice metodları isim olarak benzeseler bile birbirlerinden çok farklıdırlar.

  • slice metodu dizinin bir parçasını veya tamamını kopyalamınızı sağlar.
  • splice diziyi mutasyona uğratır (diziye yeni öğe eklemek ya da var olanı çıkartmak için kullanılır).

React’te, slice (p yok!) metodunu daha sık kullanacaksınız çünkü state’teki nesneleri veya dizileri mutasyona uğratmak istemezsiniz. Nesneleri Güncelleme sayfasında mutasyon nedir ve state için neden kullanılmamalıdır öğrenebilirsiniz.

Diziye öğe eklemek

push() metodu diziyi mutasyona uğratacaktır, ki bunu istemezsiniz:

import { useState } from 'react';

let nextId = 0;

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

  return (
    <>
      <h1>İlham verici heykeltıraşlar:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        artists.push({
          id: nextId++,
          name: name,
        });
      }}>Ekle</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Bunun yerine, mevcut öğeleri ve son eleman olarak yeni öğeyi içeren yeni diziyi oluşturun. Bunu yapmanın birden çok yolu vardır ancak en kolay yol ... dizi spread sözdizimini kullanmaktır:

setArtists( // State'i yeni bir dizi
[ // ile değiştirin
...artists, // bu eski öğelerin tümünü
{ id: nextId++, name: name } // ve sona eklenecek yeni öğeyi içerir.
]
);

Şimdi doğru şekilde çalışmakta:

import { useState } from 'react';

let nextId = 0;

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

  return (
    <>
      <h1>İlham verici heykeltıraşlar:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setArtists([
          ...artists,
          { id: nextId++, name: name }
        ]);
      }}>Ekle</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Dizi spread sözdizimi yeni öğeyi orijinal ...artists’den önceye yerleştirerek dizinin başına eklemenizi de sağlar:

setArtists([
{ id: nextId++, name: name },
...artists // Eski öğeleri dizinin sonuna yerleştir
]);

Bu şekilde, spread sözdizimi push() (öğeyi dizinin sonuna eklemek) ve unshift() (öğeyi dizinin başına eklemek) metodlarının görevini yapabilir. Yukarıdaki sandbox’ta deneyebilirsiniz!

Diziden öğe çıkartma

Diziden bir öğeyi çıkartmanın en kolay yolu o öğeyi filtrelemektir. Bir başka deyişle, o öğeyi içermeyen yeni bir dizi oluşturmaktır. Bunu yapmak için filter metodunu kullanabilirsiniz. Örneğin:

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>İlham verici heykeltıraşlar:</h1>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>
            {artist.name}{' '}
            <button onClick={() => {
              setArtists(
                artists.filter(a =>
                  a.id !== artist.id
                )
              );
            }}>
              Sil
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

“Sil” butonuna birkaç kez tıklayın ve tıklama yöneticisine bakın.

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

Burada artists.filter(a => a.id !== artist.id) ifadesi “artist dizisini kullanarak ID’leri artist.id’den farklı olan öğelerle yeni bir dizi oluştur” anlamına gelmektedir. Diğer bir deyişle, her bir artist’e karşılık gelen “Sil” butonu o artist’i diziden filtreleyecek ve nihai dizi ile yeniden render isteği gönderecektir. filter metodunun orijinal diziyi değiştirmediğini unutmayın.

Diziyi dönüştürme

Dizideki bazı ya da tüm öğeleri değiştirmek isterseniz yeni bir dizi oluşturmak için map() metodunu kullanabilirsiniz. map’e ileteceğiniz fonksiyon, verisine veya indeksine (veya her ikisine) bağlı olarak her bir öğeyle ne yapacağınızı belirler.

Bu örnekte dizi, iki daire ve bir karenin koordinatlarını içermektedir. Butona tıkladığınız zaman sadece daireler 50 piksel aşağı hareket etmektedir. Bunu, map() metodunu kullanıp yeni bir veri dizisi oluşturarak yapar:

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') {
        // Değişiklik yok
        return shape;
      } else {
        // 50 piksel aşağıda yeni bir daire döndürür
        return {
          ...shape,
          y: shape.y + 50,
        };
      }
    });
    // Yeni dizi ile yeniden render et
    setShapes(nextShapes);
  }

  return (
    <>
      <button onClick={handleClick}>
        Daireleri aşağı hareket ettir!
      </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,
        }} />
      ))}
    </>
  );
}

Dizideki öğeleri değiştirme

Bir dizideki bir veya daha fazla öğeyi değiştirmek oldukça sık istenmektedir. arr[0] = 'bird' gibi atamalar yapmak orijinal diziyi mutasyona uğrattığı için burada da map metodunu kullanmak mantıklı olacaktır.

Bir öğeyi değiştirmek için map ile yeni bir dizi oluşturun. map metodu içinde, ikinci argüman olarak öğenin indeksini alacaksınız. Bu indeksi orijinal öğeyi (fonksiyonun ilk argümanı) veya başka bir öğeyi döndürüp döndürmeyeceğinize karar vermek için kullanın:

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) {
        // Tıklanan sayacı artır
        return c + 1;
      } else {
        // Geri kalan değişmez
        return c;
      }
    });
    setCounters(nextCounters);
  }

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

Dizide belli bir konuma öğe ekleme

Bazen bir öğeyi dizinin başına ya da sonuna değil de belirli bir konuma eklemek isteyebilirsiniz. Bunu yapmak için, ... dizi spread sözdizimini slice() metodu ile beraber kullanabilirsiniz. slice() metodu diziden bir “dilim (slice)” almanızı sağlar. Yeni öğeyi eklemek için, eklemek istediğiniz konumdan önceki dilimden spread sözdizimi ile yeni bir dizi oluşturacak, ardından yeni öğeyi ekleyecek ve son olarak da orijinal dizinin geri kalanını ekleyeceksiniz.

Bu örnekte, Ekle butonu her zaman 1. indekse ekler:

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; // Herhangi bir indeks olabilir
    const nextArtists = [
      // Eklemek istediğiniz konumdan önceki öğeler:
      ...artists.slice(0, insertAt),
      // Yeni öğe:
      { id: nextId++, name: name },
      // Eklemek istediğiniz konumdan sonraki öğeler:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>İlham verici heykeltıraşlar:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Ekle
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

Dizide başka değişiklikler yapma

Spread sözdizimi veya map() ve filter() gibi diziyi mutasyona uğratmayan metodlarla yapamayacağınız bazı şeyler vardır. Örneğin, bir diziyi sıralamak veya dizi öğelerini ters çevirmek isteyebilirsiniz. JavaScript’in reverse() ve sort() metodları orijinal diziyi değiştirir, bu nedenle doğrudan bu metodları kullanamazsınız.

Ancak, önce diziyi kopyalayabilir ve sonra o dizi üzerinde değişiklikler yapabilirsiniz.

Örneğin:

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

Burada [...list] spread sözdizimi kullanılarak orijinal dizinin bir kopyası oluşturulur. Artık bir kopyanız olduğuna göre nextList.reverse() ya da nextList.sort() gibi mutasyona sebep olan metodlar kullanabilir, hatta nextList[0] = "something" ile öğeleri tek tek yeni değerlerine atayabilirsiniz.

Ancak, bir diziyi kopyalasanız bile dizinin içindeki öğeleri doğrudan mutasyona uğratamazsınız. Bunun nedeni yaptığınız kopyalamanın yüzeysel (shallow) olmasıdır. Yani yeni dizi, orijinal diziyle aynı öğeleri içermektedir. Dolayısıyla, kopyalanan dizinin içindeki bir nesneyi değiştirdiğiniz zaman mevcut state’i de mutasyona uğratmış olursunuz. Örneğin, aşağıdaki gibi bir kod sorunludur.

const nextList = [...list];
nextList[0].seen = true; // Sorun: list[0]'ı mutasyona uğratır
setList(nextList);

nextList ve list iki farklı dizi olmasına rağmen, nextList[0] ve list[0] ifadeleri aynı nesneyi işaret eder. Yani nextList[0].seen değerini değiştirirseniz, aynı zamanda list[0].seen değerini de değiştirmiş olursunuz. Bu, state’i mutasyona uğratmaktır ki bundan kaçınmalısınız! Bu sorunu iç içe JavaScript nesnelerini güncelleme yöntemine benzer şekilde, değiştirmek istediğiniz öğeleri mutasyona uğratmak yerine tek tek kopyalayarak çözebilirsiniz. Nasıl yapıldığını görelim.

Dizideki nesneleri güncelleme

Nesneler gerçekte dizilerin “içinde” yer almazlar. Yazdığınız kodda “içinde” gibi görünebilir ancak bir dizideki her nesne, dizinin “işaret ettiği” ayrı bir değerdir. Bu yüzden list[0] gibi iç içe ifadeleri değiştirirken dikkatli olmalısınız. Başka bir kişinin sanat eseri listesi (artwork list), dizinin aynı öğesine işaret edebilir!

İç içe geçmiş state’i güncellerken, güncellemek istediğiniz noktadan en üst düzeye kadar kopyalar oluşturmanız gerekir. Şimdi bunun nasıl olduğunu görelim.

Bu örnekte, iki farklı sanat eseri listesi aynı başlangıç state’ine sahiptir. Bu listelerin izole olmaları gerekirdi ancak bir mutasyon nedeniyle yanlışlıkla state’leri paylaşmaktadırlar ve listedeki bir kutuyu işaretlemek diğer listeyi de etkilemektedir:

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>Görülecek Sanat Eserleri Listesi</h1>
      <h2>Görmek istediğim eserler listesi:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Senin görmek istediğin eserler listesi:</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>
  );
}

Bu koddaki sorun şudur:

const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // Sorun: mevcut öğeyi mutasyona uğratır
setMyList(myNextList);

myNextList dizisi yeni bir dizi olmasına rağmen, dizi içindeki öğeler orijinal myList dizisindeki öğeler ile aynıdır. Yani artwork.seen’i değiştirmek orijinal sanat eserini de değiştirir. Bu sanat eseri yourList dizisinde de olduğu için hata burdan kaynaklanmaktadır. Bunun gibi hataları düşünmek zor olabilir, ancak state’i mutasyona uğratmaktan kaçınırsanız bu hatalar ortadan kalkacaktır.

Eski bir öğeyi mutasyon olmadan güncellenmiş sürümüyle değiştirmek için map metodunu kullanabilirsiniz.

setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// Değişikliklerle *yeni* bir nesne oluştur
return { ...artwork, seen: nextSeen };
} else {
// Değişiklik yok
return artwork;
}
}));

Burada ..., bir nesnenin kopyasını oluşturmak için kullanılan nesne spread sözdizimidir.

Bu yaklaşımla, mevcut state öğelerinin hiçbiri mutasyona uğramamış olur ve hata düzeltilir:

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) {
        // Değişikliklerle *yeni* bir nesne oluştur
        return { ...artwork, seen: nextSeen };
      } else {
        // Değişiklik yok
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Değişikliklerle *yeni* bir nesne oluştur
        return { ...artwork, seen: nextSeen };
      } else {
        // Değişiklik yok
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Görülecek Sanat Eserleri Listesi</h1>
      <h2>Görmek istediğim eserler listesi:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Senin görmek istediğin eserler listesi:</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>
  );
}

Genel olarak, yalnızca az önce oluşturduğunuz nesneleri mutasyona uğratmalısınız. Yeni bir sanat eseri ekliyorsanız, onu mutasyona uğratabilirsiniz, ancak zaten state’te olan bir eserde bir değişiklik yapıyorsanız, bir kopya oluşturmanız gerekmektedir.

Immer kullanarak kısa ve öz güncelleme mantığı yazmak

İç içe dizileri mutasyona uğratmadan güncellemek tıpkı nesnelerde olduğu gibi biraz tekrara binebilir:

  • Genel olarak, state’i birkaç seviyeden daha derine güncellemeniz gerekmez. Ancak state nesneleriniz çok derinse, düz (flat) olmaları için onları farklı şekilde yeniden yapılandırmak isteyebilirsiniz.
  • Eğer state yapınızı değiştirmek istemiyorsanız, Immer kullanmak isteyebilirsiniz. Immer, diziyi mutasyona uğratacak sözdizimlerini kullanmanıza izin vererek, kopyalama işlemlerini sizin yerinize kendisi yapar.

Immer ile yazılmış örneği aşağıda görebilirsiniz:

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

Immer ile artwork.seen = nextSeen gibi mutasyonların artık sorun çıkarmadığına dikkat edin:

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

Bunun nedeni, orijinal state’i mutasyona uğratmamanızdır. Burada Immer tarafından sağlanan özel bir draft nesnesini mutasyona uğratmaktayız. Benzer şekilde, draft nesnesine push() ve pop() gibi mutasyona neden olan metodları da uygulayabilirsiniz.

Arka planda Immer, draft’a yaptığınız değişikliklere göre her zaman bir sonraki state’i sıfırdan oluşturur. Bu, olay yönetecilerinizi, state’i hiç mutasyona uğratmadan kısa ve öz olarak tutar.

Özet

  • State’e dizi koyabilirsiniz ancak değiştiremezsiniz.
  • Bir diziyi mutasyona uğratmak yerine o dizinin yeni bir sürümünü oluşturun ve state’i buna göre güncelleyin.
  • [...arr, newItem] dizi spread sözdizimini kullanarak yeni öğelerle bir dizi oluşturabilirsiniz
  • filter() ve map() metodlarını kullanarak filtrelenmiş ya da dönüştürülmüş yeni diziler oluşturabilirsiniz.
  • Kodunuzu kısa ve öz tutmak için Immer’ı kullanabilirsiniz.

Problem 1 / 4:
Alışveriş sepetindeki ürünü güncelleyin

”+” butonuna tıklandığında ilgili sayının artması için handleIncreaseClick mantığını doldurun:

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