State içerisindeki nesneleri güncelleme

State, nesneler dahil olmak üzere herhangi bir JavaScript değerini tutabilir. Ancak React state içerisinde tuttuğunuz nesneleri direkt olarak değiştirmemelisiniz. Bunun yerine bir nesneyi güncellemek istediğinizde, yeni bir nesne oluşturmanız gerekmektedir (veya varolan bir nesnenin kopyasını oluşturmalısınız) daha sonra state’i kopyaladığınız nesneyi kullanması için ayarlamalısınız.

Bunları öğreneceksiniz

  • React state’i içerisinde bir nesneyi doğru şekilde güncelleyebileceksiniz.
  • İç içe bir nesneyi mutasyona uğratmadan güncelleyebileceksiniz.
  • Değişmezlik nedir, ve onu nasıl bozmadan sürdürebileceğinizi.
  • Immer ile nesne kopyalamayı daha kolay şekilde yapabileceksiniz.

Mutasyon nedir?

State içerisinde herhangi bir JavaScript değerini tutabilirsiniz.

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

Şimdiye kadar sayılarla, stringlerle ve booleanlarla çalıştınız. Bu JavaScript değerleri “değişmez” veya “salt okunur” anlamına gelir. Bir değeri değiştirmek için yeniden render işlemi yapabilirsiniz.

setX(5);

x state’i 0’ken 5 ile değiştirildi, ama 0 sayısının kendisi değişmedi. JavaScript’te, sayılar, stringler ve booleanlar gibi yerleşik temel veri tiplerinde herhangi bir değişiklik yapmak mümkün değildir.

Şimdi state içerisinde bir nesne düşünün:

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

Teknik olarak, nesnenin kendisinin içeriğini değiştirmek mümkündür. Buna mutasyon denir:

position.x = 5;

Ancak, React state içerisindeki nesneler teknik olarak değiştirilebilir olsalar da, sayılar, booleans ve dizeler gibi sözde değişmezmiş gibi muamele edilmelidir. Onları mutasyona uğratmak yerine, her zaman yenilerini oluşturmalısınız.

State’i salt okunur olarak ele alın

Başka bir deyişle, State içerisine koyduğunuz herhangi bir JavaScript nesnesini salt okunur olarak ele almalısınız.

Bu örnek, mevcut imlec pozisyonunu temsil eden bir nesneyi state içerisinde tutar. Kırmızı nokta, siz önizleme alanına dokunduğunuzda veya imleci üzerinde hareket ettirdiğinizde hareket etmesi gerekir. Ancak nokta başlangıç pozisyonunda kalıyor.

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        position.x = e.clientX;
        position.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>
  );
}

Problem bu kod parçacığıyla ilgili.

onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}

Bu kod, önceki render işleminden position değişkenine atanmış nesneyi değiştirir. Ancak state ayarlama fonksiyonunu kullanmadan, React bu nesnenin değiştiğini bilmez. Bu nedenle, React herhangi bir tepki vermez. Bu, yemeği yedikten sonra siparişin değiştirilmeye çalışılması gibi bir şeydir. State’in mutasyona uğratılması bazı durumlarda çalışabilir, ancak önermiyoruz. Render işleminde erişebildiğiniz state değerini salt okunur olarak ele almanız gerekir.

Bu durumda, gerçekten yeniden render işlemini tetiklemek için, yeni bir nesne oluşturun ve onu state ayarlama fonksiyonuna geçirin:

onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}

setPosition ile, React’a şunu söylüyorsunuz:

  • Bu yeni nesne ile position’ı değiştir
  • Ve bu bileşeni tekrar render et

Dikkat edin, kırmızı nokta şimdi önizleme alanına dokunduğunuzda veya üzerine geldiğinizde imlecinizi takip ediyor:

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

Derinlemesine İnceleme

Yerel mutasyon sorun değildir

Bu şekildeki kod, state içerisinde varolan bir nesneyi değiştirdiği için bir problemdir.

position.x = e.clientX;
position.y = e.clientY;

Ancak bu şekildeki kod kesinlikle sorunsuzdur çünkü yeni oluşturduğunuz bir nesneyi değiştiriyorsunuz;

const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

Aslında, bunu yazmakla tamamen aynı anlama geliyor:

setPosition({
x: e.clientX,
y: e.clientY
});

Mutasyon sadece state içerisinde zaten mevcut olan nesneleri değiştirdiğinizde bir problem oluşturur. Yeni oluşturduğunuz bir nesneyi değiştirmek bu nesneye henüz başka bir kod referans vermediği için tamamen sorunsuzdur. Nesneyi değiştirmek, nesneye bağlı olan bir şeyi yanlışlıkla etkileme olasılığını ortadan kaldıracaktır. Buna “yerel mutasyon” denir.

Spread sözdizimi ile nesnelerin kopyalanması

Önceki örnekte, position nesnesi her zaman mevcut imlec konumuna göre yeniden oluşturulur. Ama çoğu zaman, yeni oluşturduğunuz nesnenin bir parçası olarak mevcut verileri de dahil etmek isteyebilirsiniz. Örneğin, bir formda sadece tek bir alanı güncellemek ve diğer form alanlarının önceki değerlerini korumak isteyebilirsiniz

Bu input alanları, onChange yöneticileri state’in mutate olmasına neden oldukları için çalışmazlar:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;
  }

  function handleEmailChange(e) {
    person.email = e.target.value;
  }

  return (
    <>
      <label>
        Ad:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Soyad:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Örneğin, bu satır önceki bir render’dan state’i değiştirir.

person.firstName = e.target.value;

Aradığınız davranışı elde etmek için güvenilir yol, yeni bir nesne oluşturmak ve onu setPerson fonksiyonuna geçirmektir. Ancak burada, ayrıca alanlardan yalnızca biri değiştiği için mevcut verileri içine kopyalamak istiyorsunuz:

setPerson({
firstName: e.target.value, // New first name from the input
lastName: person.lastName,
email: person.email
});

Her bir özelliği ayrı ayrı kopyalamak zorunda kalmadan ... nesne spread sözdizimini kullanabilirsiniz.

setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});

Form şimdi çalışıyor!

Her input alanı için nasıl ayrı bir state değişkeni bildirmediğinize dikkat edin. Büyük formlar için, tüm verileri bir nesnede gruplanmış halde tutmak doğru bir şekilde güncellediğiniz sürece—çok uygundur!

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        Ad:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Soyad:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Dikkat edilmesi gereken bir nokta, ... spread sözdiziminin “yüzeysel” olmasıdır—yalnızca bir seviye derinliğe kadar kopyalar. Bu kopyalama işlemini hızlı yapar, ancak iç içe geçmiş bir özelliği güncellemek istiyorsanız, birden fazla kez kullanmanız gerekecektir.

Derinlemesine İnceleme

Birden çok alan için tek bir olay yöneticisi kullanma

Ayrıca obje tanımınızda [ and ] parantezlerini kullanarak dinamik isme sahip bir özellik belirleyebilirsiniz. İşte üç farklı olay işleyicisi yerine tek bir olay işleyicisi kullanan aynı örnek:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value
    });
  }

  return (
    <>
      <label>
        Ad:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Soyad:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          name="email"
          value={person.email}
          onChange={handleChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Burada e.target.name, <input> DOM ögesine verilen name özelliğine atıfta bulunur.

İç içe nesneleri güncelleme

Bu şekilde iç içe bir nesne yapısı düşünün:

const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});

Eğer person.artwork.city ifadesini güncellemek istiyorsanız, mutasyon ile nasıl yapılacağı açıktır:

person.artwork.city = 'New Delhi';

Ancak React’ta, state’leri değiştirilemez olarak ele alırsınız! city’i değiştirmek için, ve ardından yeni artwork’e işaret eden yeni person nesnesi oluşturmanız gerekir:

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

Veya, tek bir fonksiyon çağrısı olarak yazılır:

setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
});

Bu biraz uzun bir ifade, ancak birçok durum için gayet işe yarar:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        İsim:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Başlık:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        Şehir:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Resim:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        ({person.artwork.city} şehrinde yaşayan)
        {person.name}
        {' tarafından '}
        <br />
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

Derinlemesine İnceleme

Nesneler aslında iç içe değillerdir

Bu şekilde bir nesne kodda “iç içe” gibi gözükür:

let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};

Ancak, “iç içe yerleştirme” nesnelerin nasıl davrandığını düşünmenin yanlış bir yoludur. Kod çalıştığında, “iç içe” geçmiş nesne diye bir şey yoktur. Aslında iki farklı nesneye bakıyorsunuz:

let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};

obj1 nesnesi obj2’nin “içinde” değil. Örneğin, obj3’de obj1’e “işaret edebilir”:

let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};

let obj3 = {
name: 'Copycat',
artwork: obj1
};

obj3.artwork.city’i mutasyona uğratırsanız, hem obj2.artwork.city hem de obj1.city etkilenecektir. Bu, obj3.artwork, obj2.artwork ve obj1’in aynı nesne olduğu anlamına gelir. Nesnelerin “iç içe geçmiş” olarak düşünüldüğü zaman bu zor görülebilir. Aslında, nesneler birbirine özelliklerle “işaret eden” ayrı nesnelerdir.

Immer ile kısa güncelleme mantığı yazın

Eğer durumunuz derinlemesine iç içe ise, onu düzleştirmeyi düşünebilirsiniz. Ancak, state yapınızı değiştirmek istemiyorsanız, iç içe geçmiş spreadlere bir kısayol tercih edebilirsiniz. Immer popüler bir kütüphanedir ve size kolaylaştırılmış ancak mutasyona neden olan sözdizimini kullanarak yazmanıza izin verir ve kopyaları sizin için üretir. Immer ile yazdığınız kod, “kuralları yoksayıyormuş” gibi görünür, ancak aslında Immer, değişikliklerinizi tespit eder ve tamamen yeni bir nesne üretir:

updatePerson(draft => {
draft.artwork.city = 'Lagos';
});

Ancak normal bir mutasyonun aksine, geçmiş state’in üzerine yazmaz!

Derinlemesine İnceleme

Immer nasıl çalışır?

Immer tarafından sağlanan draft, Proxy olarak adlandırılan özel bir nesne türüdür, onunla yaptıklarınızı “kaydeder”. Bu nedenle, istediğiniz kadar serbestçe mutasyona uğratabilirsiniz! Arka planda, Immer, taslağınızın hangi kısımlarının draft edildiğini bulur ve düzenlemelerinizi içeren tamamen yeni bir nesne oluşturur.

Immer’i denemek için:

  1. Immer’i bir bağımlılık olarak eklemek için npm install use-immer komutunu çalıştırın
  2. Daha sonra import { useState } from 'react' satırını import { useImmer } from 'use-immer' ile değiştirin

Yukarıdaki örneğin Immere çevrilmiş hali şöyledir:

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

Dikkat edin, olay işleyicileri ne kadar daha kısa hale geldi. useState ve useImmer’i tek bir bileşende istediğiniz kadar karıştırabilirsiniz. Immer, özellikle state içerisinde iç içe geçme varsa ve nesnelerin kopyalanması tekrarlayan kodlara neden oluyorsa, olay işleyicilerini kısa tutmanın harika bir yoludur.

Derinlemesine İnceleme

Birkaç nedeni var:

  • Hata Ayıklama: Eğer console.log kullanır ve state’i mutasyona uğratmazsanız, önceki loglarınız daha yeni state değişiklikleri tarafından silinmeyecektir. Bu sayede, renderlar arasındaki state değişimlerini açıkça görebilirsiniz.
  • Optimizasyonlar: React’ta yaygın olarak kullanılan optimizasyon stratejileri, önceki props veya state ile sonraki props veya state’in aynı olması durumunda işlemleri atlamaya dayanır. Eğer state’in içeriğini hiç mutasyona uğratmazsanız, değişikliklerin olup olmadığını kontrol etmek çok hızlı olacaktır. Eğer prevObj === obj ise, nesne içinde hiçbir şeyin değişemeyeceğinden emin olabilirsiniz.
  • Yeni Özellikler: Yeni React özelliklerinin kullanımı, state’in bir anlık görüntü gibi davranması gibi işlem görmesiyle ilgilidir. Eğer state’in geçmiş versiyonlarını mutasyona uğratıyorsanız, bu yeni özellikleri kullanmanızı engelleyebilir.
  • Gerekli Değişiklikler: Geri/İleri işlevleri, değişikliklerin geçmişini gösterme veya kullanıcının bir formu önceki değerlere sıfırlama gibi bazı uygulama özellikleri, hiçbir şeyin mutasyona uğramadığı zaman daha kolay uygulanabilir. Bu, geçmişteki state kopyalarını hafızada tutup uygun olduğunda yeniden kullanabilmenizden kaynaklanır. Değiştirici bir yaklaşımla başlarsanız, bu gibi özellikleri sonradan eklemek zor olabilir.
  • Daha Basit Uygulama: React, nesnelerin mutasyonuna bağlı olmadığı için, nesnelerinizle özel bir işlem yapmak zorunda kalmaz. Özelliklerini ele geçirmek, her zaman Proxilere sarmak veya diğer “reaktif” çözümler gibi başlangıçta başka işler yapmak zorunda değildir. Bu aynı zamanda, React’in ek performans veya doğruluk sorunları olmadan herhangi bir—büyüklüğü önemsiz—nesneyi state içine yerleştirmenize olanak sağladığı için, React’in büyük nesneleri de dahil olmak üzere herhangi bir nesneyi state içine yerleştirmenize izin verdiği anlamına gelir.

Pratikte, React’ta state’leri mutasyona uğratarak genellikle problemlerden “kurtulabilirsiniz”, ancak bu yaklaşım göz önünde bulundurularak geliştirilen yeni React özelliklerini kullanabilmeniz için bunu yapmamanızı şiddetle tavsiye ederiz.Gelecekteki katkı sağlayıcılar ve hatta belki siz bile gelecekteki kendinize teşekkür edeceksiniz!

Özet

  • React içerisindeki bütün state’leri değiştirilemez olarak ele alın.
  • React’ta nesneleri state içinde sakladığınızda, nesneleri mutasyona uğratmak yeniden render işlemini tetiklemez ve önceki render “anlık görüntülerindeki” state’i değiştirir.
  • Bir nesneyi mutasyona uğratmak yerine, nesnenin yeni bir versiyonunu oluşturun, ve state’i nesneye ayarlayarak bir yeniden render oluşturun.
  • Nesnenin kopyasını oluşturmak için {...obj, something: 'newValue'} nesne spread sözdizimini kullanabilirsiniz.
  • Spread sözdizimi yüzeyseldir: yalnızca bir seviye derinliğe kadar kopyalar.
  • İç içe bir nesneyi güncellemek için, güncellediğiniz yerden itibaren başlayarak bütün her şeyi kopyalamalısınız.
  • Tekrarlayan kopyalama kodlarını azaltmak için Immer kullanın.

Problem 1 / 3:
Hatalı state güncellemelerini düzeltin

Bu formda birkaç hata var. Skoru arttıran butona birkaç kez tıklayın. Artmadığını fark edeceksiniz. Sonra adı düzenleyin ve skorun aniden değişikliklerinize “yetiştiğini” fark edeceksiniz. Son olarak, soyadını düzenleyin ve skorun tamamen kaybolduğunu fark edeceksiniz.

Göreviniz tüm bu hataları düzeltmektir. Hataları düzeltirken, bu hataların neden meydana geldiğini açıklayın.

import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  function handlePlusClick() {
    player.score++;
  }

  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  function handleLastNameChange(e) {
    setPlayer({
      lastName: e.target.value
    });
  }

  return (
    <>
      <label>
        Skor: <b>{player.score}</b>
        {' '}
        <button onClick={handlePlusClick}>
          +1
        </button>
      </label>
      <label>
        Ad:
        <input
          value={player.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Soyad:
        <input
          value={player.lastName}
          onChange={handleLastNameChange}
        />
      </label>
    </>
  );
}