State Yapısını Seçme

Bir bileşenin state’ini iyi yapılandırmak, değişiklik yapılması ve hata ayıklaması kolay bir bileşen ile sürekli hatalara sebep olan bir bileşen arasındaki farkı yaratabilir. Bu sayfada state’i yapılandırırken göz önünde bulundurmanız gereken ipuçlarını bulabilirsiniz.

Bunları öğreneceksiniz

  • Ne zaman tek veya birden çok state değişkeni kullanmalısınız
  • State’i düzenlerken nelerden kaçınılmalıdır
  • State yapısındaki yaygın sorunlar nasıl giderilir

State’i yapılandırmanın prensipleri

İçinde state barındıran bir bileşen yazdığınızda, bu bileşende kaç tane state değişkeni olması gerektiği veya bu değişkenlerin verilerini nasıl şekillendirmeniz gerektiği hakkında kararlar vermeniz gerekecektir. Optimalden uzak state yapısıyla dahi doğru şekilde çalışan programlar yazabilmeniz mümkün olsa da, sizi daha iyi kararlar vermeye yönlendirecek bazı prensipler aşağıdaki gibidir:

  1. Bağlantılı state değişkenlerini gruplayın. Eğer birden fazla state değişkenini hep aynı anda güncelliyorsanız, bu değişkenleri tek bir state değişkeninde birleştirmeyi değerlendirin.
  2. State çelişkilerinden kaçının. Eğer bir bileşenin state değişkenleri birbiriyle çelişecek ya da uyuşmayacak şekilde tasarlanırsa hatalara açık kapı bırakmış olursunuz. Bu durumdan kaçının.
  3. Gereksiz state oluşturmaktan kaçının. Eğer bileşeni render ederken, bileşenin prop’larından ya da varolan state değişkenlerinden ihtiyacınız olan bilgiyi hesaplayabiliyorsanız bu bilgiyi bileşenin state’inde tutmamalısınız.
  4. Yinelenen state değişkenlerinden kaçının. Aynı veri, farklı state değişkenleri ya da iç içe nesneler içinde yinelendiğinde senkronizasyonu sağlamak zorlaşabilir. Buna benzer tekrarları en aza indirgemeye çalışın.
  5. Derinlemesine iç içe olan bir state yapısından kaçının. Derinlemesine hiyerarşik yapıya sahip olan state’i güncellemek çok da pratik bir değildir. State’i daha düz bir yapıda şekillendirmeye çalışın.

Bu prensiplerin asıl amacı, state’i hatalara sebep olmadan kolayca güncelleyebilmektir. State’ten gereksiz ve yinelenen veriyi temizlemek tüm parçaların birbiriyle senkronize kalmasına yardımcı olur. Bu durum, bir veritabanı mühendisinin hata ihtimalini azaltmak için veritabanı yapısını “normalleştirmek” istemesine benzetilebilir. Albert Einstein’ın sözleriyle yorumlamak gerekirse, “State’i olabildiğince basit hale getirin; ama çok da basit olmasın.”

Haydi şimdi bu prensiplerin nasıl uygulandığını görelim.

Bazen tek bir state değişkeni mi yoksa birden fazla state değişkeni mi kullanmanız gerektiği hakkında emin olmayabilirsiniz.

Bu şekilde mi yapmalısınız?

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

Yoksa bu şekilde mi?

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

Teknik olarak ikisini de kullanabilirsiniz. Fakat iki farklı state değişkeni hep aynı anda değişiyorsa bunları tek bir state değişkeninde birleştirmek iyi bir fikir olabilir. Böylece, imleci oynatmanın her iki koordinatı da tek seferde güncellediği örnekte olduğu gibi onları sürekli senkronize tutmayı unutmazsınız:

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

Veriyi bir nesnede ya da dizide gruplayacağınız diğer bir örnek ise kaç farklı state parçasına ihtiyacınız olacağını bilemediğiniz durumlardır. Örneğin, kullanıcının özel alanlar ekleyebildiği bir form oluşturmanız gerektiğinde bu prensip size yardımcı olacaktır.

Tuzak

Eğer state değişkeniniz bir nesne ise diğer tüm alanları açıkça kopyalamadan tek bir alanı kopyalayamayacağınızı unutmayın. Örneğin, yukarıdaki örnekte setPosition({ x: 100 }) şeklinde güncelleyemezsiniz çünkü bu durumda state değişkeni y özelliğini hiç içermemiş olur! Bunun yerine, sadece x’i değiştirmek istediğinizde ya setPosition({ ...position, x: 100 }) şeklinde ya da bunları iki farklı state değişkenine bölüp setX(100) şeklinde değiştirmelisiniz.

State çelişkilerinden kaçının

İşte isSending ve isSent state değişkenlerine sahip olan bir otel geri bildirim formu:

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

// Mesajı gönderiyormuş gibi yap.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Bu kod her ne kadar çalışıyor olsa da “imkansız” state’lere açık kapı bırakıyor. Örneğin, setIsSent ve setIsSending’i birlikte çağırmayı unutursanız, isSending ve isSent değişkenlerinin aynı anda true olduğu bir durumla karşı karşıya kalabilirsiniz. Bileşeniniz ne kadar karmaşık olursa neler olduğunu anlamanız da aynı oranda zorlaşacaktır.

isSending ve isSent değişkenlerinin hiçbir zaman aynı anda true olmaması gerektiğinden bunları 'typing' (başlangıç değeri), 'sending' ve 'sent' durumlarından birini alabilen status adında tek bir state değişkenine dönüştürmek daha iyi olur:

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

// Mesajı gönderiyormuş gibi yap.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Okunabilirlik için hala sabit değişkenler oluşturabilirsiniz.

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

Fakat onlar state değişkenleri olmadığı için birbirleriyle senkronize kalıp kalmamaları hakkında endişelenmenize gerek yok.

Gereksiz state oluşturmaktan kaçının

Eğer bileşeni render ederken bileşenin prop’larından ya da varolan state değişkenlerinden ihtiyacınız olan bilgiyi hesaplayabiliyorsanız bu bilgiyi bileşenin state’inde tutmamalısınız.

Örneğin bu formu ele alın. Bu form çalışıyor fakat içinde gereksiz bir state bulabilir misiniz?

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

Bu form üç state değişkeni içeriyor: firstName, lastName ve fullName. Fakat fullName gereksiz bir state değişkeni. fullName değişkeninin değerini her zaman firstName ve lastName değişkenlerinin değerlerini kullanarak render esnasında hesaplayabilirsiniz, bu yüzden state’ten kaldırmalısınız.

Bu işlemi şu şekilde yapabilirsiniz:

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

Burada fullName bir state değişkeni değil. Bunun yerine bileşen render edilirken hesaplanıyor:

const fullName = firstName + ' ' + lastName;

Sonuç olarak, değişiklik yöneticilerinin bu değeri güncellemek için özel bir şey yapmasına gerek kalmıyor. setFirstName ve setLastName fonksiyonlarını çağırdığınızda bileşenin tekrar render edilmesini tetiklemiş olursunuz, sonrasında da fullName değişkeninin yeni değeri güncel veri kullanılarak hesaplanacaktır.

Derinlemesine İnceleme

Prop’ları state’e yansıtmayın

Gereksiz state’in yaygın bir örneği aşağıdaki gibidir:

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

Bu örnekte color, messageColor prop’u ile başlangıç değeri tanımlanan bir state değişkeni. Sorun şu ki üst bileşen daha sonra farklı bir messageColor değeri (örneğin, 'blue' yerine 'red') gönderdiğinde color state değişkeni güncellenmeyecek! Çünkü state yalnızca ilk render esnasında başlangıç değerini alır.

Bu sebeple bir prop’u state değişkenine “yansıtmak” kafa karışıklığına sebep olabilir. Bunun yerine, messageColor prop’unu direkt olarak kod içinde kullanın. Eğer daha kısa bir isim vermek istiyorsanız sabit değişken kullanın:

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

Böylece üst bileşenden gönderilen prop ile senkronizasyon sorunu oluşmayacaktır.

Prop’ları state’e “yansıtmak”, yalnızca belirli bir prop’a ait tüm güncellemeleri yok saymak istediğinizde mantıklı bir kullanımdır. Bu durumda, genel kabul olarak, gönderilecek yeni değerlerin yok sayıldığını belirtmek için prop adını initial veya default ön ekiyle başlatın:

function Message({ initialColor }) {
// `color` state değişkeni `initialColor`'ın *ilk* değerini tutar.
// `initialColor`'a ait sonraki tüm değişiklikler yok sayılır.
const [color, setColor] = useState(initialColor);

Yinelenen state değişkenlerinden kaçının

Aşağıdaki menü listesi bileşeni birden fazla atıştırmalık içinden birini seçmenizi sağlar:

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

Mevcut durumda, seçilen öğe selectedItem state değişkeninde nesne olarak saklanır. Ancak bu durum pek de iyi değil çünkü selectedItem’ın içeriği items listesinde tutulan nesne ile aynı. Bu durum, öğe hakkındaki bilginin iki farklı yerde yinelenerek tutulduğu anlamına gelir.

Peki bu neden bir sorun? Haydi her öğeyi düzenlenebilir yapalım:

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

Dikkat ederseniz önce bir öğeye ait “Choose” butonuna tıklar ve sonrasında o öğeyi düzenlerseniz girdi güncellenecektir fakat aşağıdaki öğe adı yapılan değişikliği yansıtmayacaktır. Bu yinelenen bir state’e sahip olmanızdan kaynaklanır ve selectedItem’ı güncellemeyi unuttuğunuz anlamına gelir.

selectedItem’ı güncelleyebilecek olsanız dahi, daha basit bir çözüm yinelemeyi ortadan kaldırmaktır. Bu örnekte selectedItem nesnesi (items içindeki nesnelerle yineleme durumunu yaratan nesne) yerine state’te selectedId değerini tutup sonrasında items dizisinde bu ID’ye sahip olan selectedItem’ı bulabilirsiniz:

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

(Alternatif olarak seçilen indeksi state’te tutabilirsiniz.)

State aşağıdaki gibi yineleniyordu:

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

Fakat değişiklik sonrasında aşağıdaki gibi oldu:

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

Yineleme durumu ortadan kalktı ve yalnızca gerekli olan state’i tutuyorsunuz!

Artık seçili öğeyi düzenlediğinizde altındaki mesaj anında güncellenecektir. Bunun sebebi setItems’ın bileşenin tekrar render edilmesine sebep olması ve items.find(...)’ın başlığı güncellenen öğeyi bulmasıdır. Yani seçili öğeyi tutmanıza gerek yoktur çünkü sadece seçili ID gereklidir. Geri kalan bilgi render esnasında hesaplanabilir.

Derinlemesine iç içe olan bir state yapısından kaçının

Gezegenleri, kıtaları ve ülkeleri içeren bir seyahat planı hayal edin. Bu durumda state yapısını, iç içe nesne ve dizilerden oluşacak şekilde tasarlamak sizi cezbedebilir:

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

Hali hazırda ziyaret ettiğiniz bir lokasyonu silmeye yarayan bir buton eklemek istediğinizi varsayalım. Nasıl yapardınız? İç içe yapılandırılmış state’i güncellemek için değiştiği yere kadar olan tüm nesnelerin kopyalarını yaratmak gerekir. Derinlemesine iç içe geçmiş bir lokasyonu silmek için ise tüm üst lokasyon zincirini kopyalamak gerekir. Böyle bir kod gereğinden daha uzun olabilir.

Eğer state güncellemek için gereğinden daha fazla iç içe geçmiş bir halde ise onu daha düz bir yapıya getirmeyi değerlendirin. İşte bu veriyi yeniden yapılandırmanız için bir yol. Eğer her place (lokasyon) alt lokasyonları için ağaç yapısında bir diziye sahipse her place’in (lokasyonun) alt lokasyonlarının IDlerini tuttuğu bir diziye sahip olmasını sağlayabilirsiniz. Sonrasında da her bir lokasyon ID’sine karşılık gelen lokasyonun eşlemesini tutabilirsiniz.

Veriyi aşağıdaki gibi yeniden yapılandırmak size bir veritabanı tablosunu hatırlatabilir:

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

Artık state “düz” (“normalleştirilmiş” olarak da bilinir), iç içe öğeleri güncellemek daha kolay.

Artık bir lokasyonu kaldırmak için state’in yalnızca iki seviyesini güncellemeniz gerekiyor.

  • Üst lokasyonunun güncellenmiş sürümü kaldırılan ID’yi childIds dizisinden çıkarmalı.
  • Kök “tablo” nesnesinin güncellenmiş sürümü üst lokasyonun güncellenmiş halini içermeli.

İşte bunu nasıl yapabileceğinizin bir örneği:

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];
    // Üst lokasyonun, bu alt ID'yi içermeyen
    // yeni bir sürümünü oluştur
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Kök state nesnesini güncelle...
    setPlan({
      ...plan,
      // ...böylece güncellenen üst öğeye sahip olsun.
      [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>
  );
}

State’i istediğiniz kadar iç içe olacak şekilde yapılandırabilirsiniz. Fakat “düz” yapılandırmak birçok sorunu çözebilir. Hem state’i güncellemeyi kolaylaştırır hem de iç içe nesnenin farklı bölümlerinde yinelenme ihtimalini ortadan kaldırmaya yardımcı olur.

Derinlemesine İnceleme

Bellek kullanımını iyileştirmek

İdeal olarak, bellek kullanımını iyileştirmek için silinen öğeleri (ve alt öğelerini!) “tablo” nesnesinden kaldırmalısınız. Bu sürüm bunu gerçekleştirir. Aynı zamanda güncelleme mantığını daha kısa ve öz hale getirmek için Immer kullanır.

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

Bazı durumlarda iç içe state’in bazı parçalarını alt bileşenlere taşıyarak state’i iç içe yapılandırmayı azaltabilirsiniz. Bu durum çok kısa süren, saklanması gerekmeyen, UI state’lerinde işe yarar, örneğin bir öğenin üzerine gelindiğinde (hover edildiğinde).

Özet

  • Eğer iki farklı state değişkeni hep aynı anda güncelleniyorsa bunları tek bir state değişkeninde birleştirmeyi değerlendirin.
  • ”İmkansız” state’ler oluşturmaktan kaçınmak için state değişkenlerini dikkatle seçin.
  • State’inizi, onu güncellerken hata yapma ihtimalini en aza indirgeyecek şekilde yapılandırın.
  • State’inizi senkronize tutmaya çalışmak yerine gereksiz ve yinelenen şekilde yapılandırmaktan kaçının.
  • Güncellemeleri engellemek istediğiniz durumlar haricinde prop’ları state içine koymayın.
  • Seçim (selection) gibi UI kalıpları için state’te objenin kendisi yerine IDsini ya da indeksini tutun.
  • Eğer derinlemesine iç içe geçmiş state’i güncellemek karmaşıksa onu düz bir yapıya getirmeye çalışın.

Problem 1 / 4:
Güncellenmeyen bir bileşeni düzeltin

Clock bileşeni iki prop alır: color ve time. Seçim kutusunda farklı bir renk seçtiğinizde, Clock bileşeni üst bileşeninden farklı bir color prop’u alır. Fakat, bir sebepten ötürü gösterilen renk güncellenmiyor. Neden? Bu sorunu çözün.

import { useState } from 'react';

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