Girdiye State ile Reaksiyon Verme

React, UI’ı manipüle etmek için bildirimsel bir yol sağlar. UI’ın her bir parçasını doğrudan manipüle etmek yerine, bileşeninizin içinde bulunabileceği farklı durumları (state’leri) tanımlar ve kullanıcı girdisine karşılık bunlar arasında geçiş yaparsınız. Bu, tasarımcıların UI hakkındaki düşüncelerine benzer.

Bunları öğreneceksiniz

  • Bildirimsel UI programlamanın, zorunlu UI programlamadan nasıl ayrıştığını
  • Bileşeninizin içinde bulunabileceği farklı görsel state’lerin nasıl sıralanabileceğini
  • Farklı görsel state’ler arasındaki değişimin koddan nasıl tetiklenebileceğini

Bildirimsel ve zorunlu UI’ın karşılaştırması

UI etkileşimleri tasarlarken, UI’ın kullanıcı aksiyonlarına karşılık nasıl değiştiği hakkında muhtemelen düşünürsünüz. Kullanıcının bir cevap gönderdiği bir form düşünelim:

  • Formun içine bir şey yazdığınızda, “Gönder” butonu etkinleşir.
  • ”Gönder” butonuna bastığınızda, hem form hem de buton devre dışı kalır ve bir spinner görünür.
  • Eğer ağ isteği başarılı olursa, form görünmez hale gelir ve “Teşekkürler” mesajı görünür.
  • Eğer ağ isteği başarısız olursa, bir hata mesajı görünür ve form tekrar etkinleşir.

Zorunlu programlamada yukarıdakiler, etkileşimi nasıl uyguladığınıza doğrudan karşılık gelir. Az önce ne olduğuna bağlı olarak UI’ı manipüle etmek için tam talimatları yazmanız gerekir. Bunu düşünmenin başka bir yolu da şudur: Arabada birinin yanına bindiğinizi ve ona adım adım nereye gideceğini söylediğinizi hayal edin.

JavaScript'i temsil eden endişeli görünümlü bir kişi tarafından sürülen bir arabada, bir yolcu sürücüye bir dizi karmaşık dönüşü gerçekleştirmesini emreder.

Rachel Lee Nabors tarafından görselleştirilmiştir.

Nereye gitmek istediğinizi bilmiyor, sadece talimatlarınızı takip ediyor. (Ve eğer yanlış yönlendirirseniz, kendinizi yanlış yerde bulursunuz!) Buna zorunlu denir çünkü, bilgisayara UI’yı nasıl güncellemesi gerektiğini, spinner’dan butona her eleman için “talimat vermeniz” gerekir.

Zorunlu UI programlamanın bu örneğinde form, React kullanmadan oluşturulmuştur. Sadece tarayıcı DOM’unu kullanır:

async function handleFormSubmit(e) {
  e.preventDefault();
  disable(textarea);
  disable(button);
  show(loadingMessage);
  hide(errorMessage);
  try {
    await submitForm(textarea.value);
    show(successMessage);
    hide(form);
  } catch (err) {
    show(errorMessage);
    errorMessage.textContent = err.message;
  } finally {
    hide(loadingMessage);
    enable(textarea);
    enable(button);
  }
}

function handleTextareaChange() {
  if (textarea.value.length === 0) {
    disable(button);
  } else {
    enable(button);
  }
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

function enable(el) {
  el.disabled = false;
}

function disable(el) {
  el.disabled = true;
}

function submitForm(answer) {
  // Ağa istek atıyormuş gibi yapalım.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (answer.toLowerCase() === 'istanbul') {
        resolve();
      } else {
        reject(new Error('İyi tahmin ama yanlış cevap. Tekrar dene!'));
      }
    }, 1500);
  });
}

let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;

UI’yı zorunlu bir şekilde manipüle etmek izole örneklerde yeterince işe yarar, fakat bunun daha karmaşık sistemlerde yönetilmesi katlanarak zorlaşır. Bunun gibi farklı formlarla dolu bir sayfayı güncellediğinizi hayal edin. Yeni bir UI elemanı veya yeni bir etkileşim eklemek, bir hata (örneğin, bir şeyi göstermeyi veya gizlemeyi unutmak) eklemediğinizden emin olmak için mevcut tüm kodun dikkatlice kontrol edilmesini gerektirir.

React, bu sorunu çözmek için geliştirilmiştir.

React’ta, UI’yı doğrudan manipüle etmezsiniz—yani bileşenleri doğrudan etkinleştirmez, devre dışı bırakmaz, göstermez veya gizlemezsiniz. Bunun yerine, göstermek istediğiniz şeyi belirtirsiniz ve React, UI’yı nasıl güncelleyeceğini çözer. Bir taksiye bindiğinizi ve şoföre nereden döneceğini söylemek yerine nereye gitmek istediğinizi söylediğinizi düşünün. Sizi oraya götürmek şoförün işidir ve hatta belki de sizin aklınıza gelmeyen kestirme yollar biliyordur.

In a car driven by React, a passenger asks to be taken to a specific place on the map. React figures out how to do that.

Rachel Lee Nabors tarafından görselleştirilmiştir.

UI’ı bildirimsel olarak düşünmek

Yukarıda bir formun zorunlu olarak nasıl uygulanacağını gördünüz. React’ta nasıl düşüneceğinizi daha iyi anlamak için, aşağıda bu UI’ı React’ta tekrar uygulayacaksınız:

  1. Bileşeninizin farklı görsel state’lerini tanımlayın
  2. Bu state değişikliklerini neyin tetiklediğini belirleyin
  3. State’i useState ile bellekte gösterin
  4. Gereksiz tüm state değişkenlerini kaldırın
  5. State’i ayarlamak için olay yöneticilerini bağlayın

Adım 1: Bileşeninizin farklı görsel state’lerini tanımlayın

Bilgisayar biliminde, bir “state machine”in birçok “state”ten birinde olduğunu duyabilirsiniz. Eğer bir tasarımcı ile çalışıyorsanız, farklı “görsel state”ler için modeller görmüş olabilirsiniz. React, tasarım ve bilgisayar biliminin kesiştiği noktada durmaktadır, bu nedenle bu fikirlerin her ikisi de ilham kaynağıdır.

Öncelikle, UI’ın kullanıcının görebileceği tüm farklı “state”lerini görselleştirmeniz gerekir:

  • Empty: Formda devre dışı bırakılmış bir “Gönder” butonu var.
  • Typing: Formda etkinleştirilmiş bir “Gönder” butonu var.
  • Submitting: Form tamamen devre dışı bırakılmış. Spinner gösterilir.
  • Success: Form yerine “Teşekkürler” mesajı gösterilir.
  • Error: Typing state’i ile aynı ama ekstra bir hata mesajı ile.

Tıpkı bir tasarımcı gibi, mantık eklemeden önce farklı state’ler için modeller oluşturmak isteyeceksiniz. Örneğin, burada formun sadece görsel kısmı için bir model bulunmaktadır. Bu model, varsayılan değeri 'empty' olan status adlı bir prop tarafından kontrol edilir:

export default function Form({
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>Doğru cevap!</h1>
  }
  return (
    <>
      <h2>Şehir sorusu</h2>
      <p>
        Havayı içilebilir suya çeviren reklam tabelası hangi şehirdedir?
      </p>
      <form>
        <textarea />
        <br />
        <button>
          Gönder
        </button>
      </form>
    </>
  )
}

Bu prop’u istediğiniz gibi isimlendirebilirsiniz, isimlendirme önemli değil. Başarı mesajını görmek için status = 'empty'’yi status = 'success'’e değiştirmeyi deneyin. Modelleme, herhangi bir mantığı bağlamadan önce UI’ın üzerinden hızlıca tekrar geçmenizi sağlar. İşte aynı bileşenin hala status prop’u tarafından “kontrol edilen” daha ayrıntılı bir prototipi:

export default function Form({
  // Şunları deneyin: 'submitting', 'error', 'success':
  status = 'empty'
}) {
  if (status === 'success') {
    return <h1>Doğru cevap!</h1>
  }
  return (
    <>
      <h2>Şehir sorusu</h2>
      <p>
        Havayı içilebilir suya çeviren reklam tabelası hangi şehirdedir?
      </p>
      <form>
        <textarea disabled={
          status === 'submitting'
        } />
        <br />
        <button disabled={
          status === 'empty' ||
          status === 'submitting'
        }>
          Gönder
        </button>
        {status === 'error' &&
          <p className="Error">
            İyi tahmin ama yanlış cevap. Tekrar dene!
          </p>
        }
      </form>
      </>
  );
}

Derinlemesine İnceleme

Birden fazla görsel state’i tek seferde görüntüleme

Eğer bir bileşenin birçok görsel state’i varsa, hepsini tek bir sayfada göstermek uygun olabilir:

import Form from './Form.js';

let statuses = [
  'empty',
  'typing',
  'submitting',
  'success',
  'error',
];

export default function App() {
  return (
    <>
      {statuses.map(status => (
        <section key={status}>
          <h4>Form ({status}):</h4>
          <Form status={status} />
        </section>
      ))}
    </>
  );
}

Böyle sayfalar sıklıkla “yaşayan stil rehberi” veya “hikaye kitabı” diye adlandırılır.

Adım 2: State değişikliklerini neyin tetiklediğini belirleyin

State güncellemelerini iki girdi çeşidine karşılık tetikleyebilirsiniz:

  • İnsan girdileri: bir butona tıklama, bir alana yazma, bir bağlantıya gitme vb.
  • Bilgisayar girdileri: bir ağ yanıtının ulaşması, bir zamanaşımının tamamlanması, bir görüntünün yüklenmesi vb.
Bir parmak.
İnsan girdileri
Birler ve sıfırlar.
Bilgisayar girdileri

Rachel Lee Nabors tarafından görselleştirilmiştir.

Her iki durumda da UI’ı güncellemek için state değişkenlerini ayarlamak zorundasınız. Geliştirdiğiniz form için, birkaç farklı girdiye karşılık state değiştirmeniz gerekecek:

  • Metin girdisini değiştirmek (insan) metin kutusunun boş olup olmadığına bağlı olmak üzere, state’i Empty’den Typing’e ya da tam tersine çevirmeli.
  • Gönder butonuna tıklamak (insan) state’i Submitting’e çevirmeli.
  • Başarılı ağ yanıtı (bilgisayar) state’i Success’e çevirmeli.
  • Başarısız ağ yanıtı (bilgisayar) state’i ilgili hata mesajı ile birlikte Error’a çevirmeli.

Not

İnsan girdilerinin sıklıkla olay yöneticilerini gerektirdiğine dikkat ediniz!

Bu akışı görselleştirmeye yardımcı olması için, kağıt üzerinde her bir state’i etiketli bir daire olarak ve iki state arasındaki her bir değişikliği bir ok olarak çizmeyi deneyin. Bu şekilde birçok akışı taslak olarak çizebilir ve uygulamadan çok önce hataları ayıklayabilirsiniz.

Soldan sağa doğru 5 düğümle ilerleyen akış şeması. 'empty' olarak etiketlenen ilk düğümün 'start typing' olarak etiketlenen ve 'typing' olarak etiketlenen bir düğüme bağlı bir kenarı vardır. Bu düğümün iki kenarı olan 'submitting' etiketli bir düğüme bağlı 'press submit' etiketli bir kenarı vardır. Sol kenar 'error' etiketli bir düğüme bağlanan 'network error' olarak etiketlenmiştir. Sağ kenar ise 'success' etiketli bir düğüme bağlanan 'network success' olarak etiketlenmiştir.
Soldan sağa doğru 5 düğümle ilerleyen akış şeması. 'empty' olarak etiketlenen ilk düğümün 'start typing' olarak etiketlenen ve 'typing' olarak etiketlenen bir düğüme bağlı bir kenarı vardır. Bu düğümün iki kenarı olan 'submitting' etiketli bir düğüme bağlı 'press submit' etiketli bir kenarı vardır. Sol kenar 'error' etiketli bir düğüme bağlanan 'network error' olarak etiketlenmiştir. Sağ kenar ise 'success' etiketli bir düğüme bağlanan 'network success' olarak etiketlenmiştir.

Form state’leri

Adım 3: State’i useState ile bellekte gösterin

Şimdi bileşeninizin görsel state’lerini useState ile bellekte göstermeniz gerekecek. Kilit nokta sadelik: state’in her bir parçası birer “hareketli parça”dır, ve mümkün olduğunca az “hareketli parça” istersiniz. Daha fazla karmaşıklık daha fazla hataya yol açar!

Kesinlikle bulunması gereken state ile başlayın. Örneğin, girdi için answer ve (eğer varsa) son hata için errorı depolamanız gerekecek.

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);

Sonrasında, göstermek istediğiniz görsel state’i temsil eden bir state değişkenine ihtiyacınız olacak. Bunu bellekte temsil etmenin genellikle birden fazla yolu vardır, bu nedenle denemeler yapmanız gerekecek.

Hemen en iyi yolu bulmakta zorlanırsanız, tüm muhtemel görsel state’leri kapsandığından kesinlikle emin olacağınız kadar state ekleyerek başlayın:

const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);

İlk fikriniz muhtemelen en iyisi olmayacaktır, ancak sorun değil—state’i yeniden düzenlemek sürecin bir parçasıdır!

Adım 4: Gereksiz tüm state değişkenlerini kaldırın

State içeriğinde kopyalardan kaçınmak istersiniz ve bu yüzden yalnızca gerekli olanların takibini yaparsınız. State yapınızı yeniden düzenlemek için biraz zaman harcamak bileşenlerinizin anlaşılmasını kolaylaştıracak, tekrarları azaltacak ve istenmeyen anlamları önleyecektir. Amacınız bellekteki state’in kullanıcının görmesini isteyeceğiniz geçerli bir UI’ı temsil etmediği durumları önlemektir. (Örneğin, asla bir hata mesajı göstermek ve aynı zamanda girişi devre dışı bırakmak istemezsiniz, aksi takdirde kullanıcı hatayı düzeltemez!)

İşte state değişkenleriniz hakkında sorabileceğiniz bazı sorular:

  • Bu state bir paradoksa sebep oluyor mu? Örneğin, hem isTyping hem de isSubmitting, true olamaz. Paradoks genellikle state’in yeterince kısıtlanmadığı anlamına gelir. İki boolean’ın dört muhtemelen kombinasyonu vardır, ancak sadece üç tanesi geçerli state’e karşılık gelir. “İmkansız” state’i kaldırmak için bunları, değeri 'typing', 'submitting', veya 'success'’ten biri olması gereken status içinde birleştirebilirsiniz
  • Aynı bilgi başka bir state değişkeninde zaten mevcut mu? Bir diğer paradoks: isEmpty ve isTyping aynı anda true olamazlar. Bunları ayrı state değişkenleri haline getirerek, senkronize olmamaları ve hatalara neden olmaları riskini alırsınız. Neyse ki, isEmpty’yi kaldırıp yerine answer.length === 0’ı kontrol edebilirsiniz.
  • Aynı bilgiye bir başka state değişkeninin tersini alarak erişebiliyor musunuz? error !== null kontrolünü yapabildiğiniz için isError’a ihtiyacınız yok.

Bu temizlikten sonra, 3 tane (7’den düştü!) gerekli state değişkeniniz kalıyor:

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', veya 'success'

Bunların gerekli olduğunu biliyorsunuz, çünkü işlevselliği bozmadan hiçbirini kaldıramazsınız.

Derinlemesine İnceleme

“İmkansız” state’leri bir reducer ile elimine etme

Bu üç değişken, bu formun state’ini yeterince iyi bir şekilde temsil ediyor. Ancak, hala tam olarak anlam ifade etmeyen ara state’ler var. Örneğin, status 'success' olduğunda null olmayan bir error bir anlam ifade etmez. State’i daha kesin bir şekilde modellemek için, onu bir reducer’a çıkarabilirsiniz. Reducer’lar, birden fazla state değişkenini tek bir nesnede birleştirmenize ve ilgili tüm mantığı bir araya getirmenize olanak tanır!

Adım 5: State’i ayarlamak için olay yöneticilerini bağlayın

Son olarak, state’i güncelleyen olay yöneticileri oluşturun. Aşağıda, tüm olay yöneticilerinin bağlandığı formun son hali yer almaktadır:

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>Doğru cevap!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>Şehir sorusu</h2>
      <p>
        Havayı içilebilir suya çeviren reklam tabelası hangi şehirdedir?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Gönder
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Ağa istek atıyormuş gibi yapalım.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('İyi tahmin ama yanlış cevap. Tekrar dene!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Bu kod orijinal bildirimsel örnekten daha uzun olmasına rağmen, çok daha az kırılgandır. Tüm etkileşimleri state değişiklikleri olarak ifade etmek, daha sonra mevcut state’leri bozmadan yeni görsel state’ler eklemenizi sağlar. Ayrıca, etkileşimin kendi mantığını değiştirmeden her bir state’te neyin görüntülenmesi gerektiğini değiştirmenize de olanak tanır.

Özet

  • Bildirimsel programlama, UI’ın mikro yönetimi (zorunlu) yerine, UI’ı her bir görsel state için tarif etmek anlamına gelir.
  • Bir bileşen geliştirirken:
    1. Tüm görsel state’lerini tanımlayın.
    2. State değişiklikleri için insan ve bilgisayar tetikleyicilerini belirleyin.
    3. State’i, useState ile modelleyin.
    4. Hataları ve paradoksları önlemek için gereksiz state’i kaldırın.
    5. State’i ayarlamak için olay yöneticilerini bağlayın.

Problem 1 / 3:
Bir CSS sınıfı ekleyin ve çıkarın

Problemi, resme tıklandığında dış <div>’den background--active CSS sınıfını kaldıracak, ancak <img>’ye picture--active sınıfını ekleyecek şekilde çözün. Arka plana tekrar tıklandığında orijinal CSS sınıfları geri yüklenmeli.

Görsel olarak, resmin üzerine tıkladığınızda mor arka planın kaldırılmasını ve resim kenarlığının vurgulanmasını beklemelisiniz. Resmin dışına tıklamak arka planı vurgular, ancak resim kenarlığı vurgusunu kaldırır.

export default function Picture() {
  return (
    <div className="background background--active">
      <img
        className="picture"
        alt="Kampung Pelangi'deki (Endonezya) gökkuşağı evleri"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}