React Hook

Hooklar, bir class yazmadan state ve diğer React özelliklerini kullanmanıza izin veren yeni bir özellik önerisidir. Şu anda React v16.7.0-alpha’da ve açık bir RFC‘de tartışılıyor.

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // componentDidMount ve componentDidUpdate'e benzer:
  useEffect(() => {
    // Sayfa başlığını günceller.
    document.title = `${count} kere tıkladınız`;
  });

  return (
    <div>
      <p>{count} kere tıkladınız.</p>
      <button onClick={() => setCount(count + 1)}>
        Tıkla
      </button>
    </div>
  );
}

Verilerin getirilmesi, bir abonelik oluşturulması ve React componentlerinde DOM’un manuel olarak değiştirilmesi, tüm yan efektlere örnektir.

İpucu

React class lifecyle fonksiyonlarına aşina iseniz, useEffect fonksiyonunu componentDidMount, componentDidUpdate ve componentWillUnmount birleşimi olarak düşünebilirsiniz.

useEffect = componentDidMount + componentDidUpdate + componentWillUnmount

Lifecyle Fonksiyonları hakkında detaylı bilgi için tıklayınız.

Temizleme Yapmadan Efektler

Bazen, React DOM’u güncelledikten sonra bazı kodlar çalıştırmak isteriz. Network istekleri, manuel DOM mutasyonları ve log kaydı, temizleme gerektirmeyen efektlere genel örneklerdir. Bunu söylüyoruz çünkü onları çalıştırabilir ve hemen onları unutabiliriz. Classların ve Hooks’un bu gibi yan efektleri nasıl ifade edeceğimizi karşılaştıralım.

Classları Kullanan Örnek

React class componentlerinde, render fonksiyonunun kendisi yan efektlere neden olmamalıdır. Çok erken olurdu - genellikle efektleri React DOM’u güncelledikten sonra gerçekleşmesini istiyoruz.

Bu nedenle, React classlarında, componentDidMount ve componentDidUpdate componentlerine yan etkiler koyarız. Örneğimize geri dönersek, burada React, DOM’da değişiklikler yaptıktan hemen sonra belge başlığını güncelleyen bir sayaç class componentidir:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `${this.state.count} kere tıkladınız`;
  }

  componentDidUpdate() {
    document.title = `${this.state.count} kere tıkladınız`;
  }

  render() {
    return (
      <div>
        <p>${this.state.count} kere tıkladınız.</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Tıkla
        </button>
      </div>
    );
  }
}

Classlardaki bu iki lifecyle fonksiyonu arasında gördüğünüz gibi aynı kodu kopyaladık.

Bunun nedeni, birçok durumda componentlerin yeni render edilmiş olup olmadığına veya güncellenip güncellenmediğine bakılmaksızın, aynı yan efekti gerçekleştirmek istiyoruz. Kavramsal olarak, her renderdan sonra olmasını isteriz - ancak React class componentlerin böyle bir fonksiyonu yoktur. Ayrı bir fonksiyon yazabilirdik ama yine de iki lifecyle fonksiyonunda bu oluşturduğumuz fonksiyonu çağırmak zorundayız.

Şimdi useEffect hook ile aynın şeyi nasıl yapacağımıza bakalım.

Hook Kullanımına Örnek

Bu örneği zaten üstte gördük fakat tekrar yakından bakalım:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `${count} kere tıkladınız`;
  });

  return (
    <div>
      <p>{count} kere tıkladınız.</p>
      <button onClick={() => setCount(count + 1)}>
        Tıkla
      </button>
    </div>
  );
}

useEffect ne yapar? Bu hook’u kullanarak, componentin render edildikten sonra bir şeyler yapması gerektiğini React’a söylemiş olursunuz. React, bu fonksiyonu hatırlayacaktır (buna “effect” diyeceğiz) ve DOM güncellemelerini gerçekleştirdikten sonra bu effect’te sayfa title’ını belirledik, ancak veri getirmeyi veya başka bir API’ye çağrı yapabiliriz.

Neden useEffect bir componentin içerisinde çağrılır? Componentin içine useEffect yerleştirmek, effectten count state değişkenine (veya herhangi bir propsa) erişmemize izin verir. Okumak için özel bir API’ye ihtiyacımız yok (çünkü zaten fonksiyon scope kapsamında). Hooklar, JavaScript kapanışlarını benimserler ve JavaScript’in zaten çözüm sağladığı React-Specifis API’lerini tanıtmaktan kaçınırlar.

useEffect her renderdan sonra çalışır mı Evet! Varsayılan olarak her güncellemeden sonra ilk render ve sonrasında da çalışır (daha sonradan bunu nasıl özelleştireceğimizi konuşacağız). Mounting ve updating terimleriyle düşünmek yerine, bunu düşünmek daha kolay olabilir. Effectler render edildikten sonra olur. React, DOM’un etkileri çalıştırdığı zaman güncellenediğini garanti eder.

Detaylı Açıklama

Artık effectler hakkında daha fazla bilgi sahibi olduğumuza göre, bu kodlar mantıklı olmalı:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `${count} kere tıkladınız.`;
  });

count state değişkenini alırız ve sonra React’a bir effect kullanmamız gerektiğini söyleriz. useEffect hookuna bir fonksiyon iletiyoruz (setCount); bu fonksiyon bizim effectimizdir. Effectimizin içine sayfa title’ını document.title tarayıcı API’sini kullanarak değiştirdik. Effectin içinde en son countu okuyabiliriz çünkü bu bizim fonksiyon scope’undadır. React, componentimizi oluşturduğunda, kullandığımız effecti hatırlar ve DOM’u güncelledikten sonra effectimizi çalıştırır. Bu ilk render dahil olmak üzere her render için geçerlidir.

Deneyimli JavaScript geliştiricileri, useEffect fonksiyonuna iletilen fonksiyonun her renderdan sonra farklı olacağını fark edebilir, bu kasıtlıdır. Aslında bu, count değerini effectin içinden okumamızı sağlar. Her yeniden render edildiğinde bir öncekinin yerine farklı bir effecti programlıyoruz. Bir bakıma bu, effectlerin render sonucunun bir parçası gibi davrandığını gösterir. Bunun neden yararlı olduğunu daha sonra göreceğiz.

İpucu

componentDidMount veya componentDidUpdate fonksiyonlarının aksine useEffect ile planlanan effectler, tarayıcıyı ekranın güncellemesini engellemez. Bu uygulamanızı daha duyarlı hissettirir. Effectlerin çoğunun senkronie olmasına gerek yoktur. Yaptıkları nadir durumlarda (layout’un ölçülmesi gibi) useEffect ile aynı API’ye sahip ayrı bir useLayoutEffect vardır.

Temizleme ile Efektler

Daha önce, temizleme gerektirmeyen yan etkilerin nasıl oluşturulduğunu inceledik. bazı hari veri kaynaklarına bir abonelik ayarlamak isteyebiliriz. Bu durumda, bellek sızıntısına neden olmamak için temizlik yapılması önemlidir. Bunu sınıflarla ve hook ile yapabileceğimi inceleyelim.

Sınıfları Kullanarak Örnek

React sınıflarında, ComponentDidMount içinde bir abonelik kurarsanız, bunu ComponentWillUnmount içinde temizlersiniz. Örneğin, bir arkadaşının çevrimiçi durumuna abone olmamızı sağlayan bir ChatAPI modülümüz olduğunu varsayalım. Bir sınıf kullanarak nasıl yazabileceğini aşağıda bulabilirsiniz.

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

componentDidMount ve componentWillunmount birbirlerini yansıtmaları gerektiğine dikkat edin. Lifecyle fonskiyonları, ikisininde kavramsal olarak aynı etkiyle ilişki olmasına rağmen, bunu iki kere yazmamızı sağlar.

Not

Eagle-eyed okuyucular, bu örneğin tamamen doğru olması için bir componentDidUpdate fonksiyonuna ihtiyaç duyduklarını fark edebilirler. Şimdilik bunu görmezden geleceğiz (sonradan bahsedilecek).

Hooks Kullanarak Örnek

Aynı bileşeni Hooks ile nasıl yazabileceğimize bir bakalım.

Temizliği gerçekleştirmek için ayrı bir efekte ihtiyacımız olacağını düşünüyor olabilirsiniz. Ancak bir abonelik ekleme ve kaldırma kodu, o kadar sıkı bir şekilde ilişkilidir ki useEffect onu bir arada tutmak için tasarlanmıştır. Efektiniz bir fonksiyon return ederse, temizleme zamanı geldiğinde React onu çalıştıracaktır.

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Bu kısımda nasıl temizleneceğini belirtin:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

Neden efektimizden bir fonskiyon return ettik? Bu, efektler için isteğe bağlı temizleme mekanizmasıdır. Her efekt, ondan sonra temizleyen bir fonksiyon return edebilir. Bu, abonelik ekleme ve kaldırma mantığını korumamızı sağlar. Onlar aynı efektin bir parçası!

React tam olarak ne zaman bir efekte neden oluyor? React, component unmounts olduğunda React temizleme işlemini gerçekleştiriyor. Ancak, daha önce öğrendiğimiz gibi efektler sadece bir kez değil, her render için çalışır. Bu yüzden React ayrıca efektleri bir dahaki sefere çalıştırmadan önce önceki renderdeki efektleri temizler. Bunun neden hatalardan kaçınmaya yardımcı olduğunu ve daha sonra performans sorunları yaratması durumunda bu davranıştan nasıl vazgeçileceğini ilerleyen kısımlarda göreceğiz.

Not

İsimlendirilmiş bir fonksiyonu efekkten döndürmek zorunda değiliz. Amacını netleştirmek için buraya cleanup diyoruz, ancak bir arrow fonksiyonu return edebilir veya farklı bir şey diyebilirsiniz.

Recap

useEffect öğresinin bir component render edildikten sonra yan efektleri ifade etmemize izin verdiğini öğrendik. Bazı efektler temizleme gerektirebilir, bu nedenli bir fonksiyon return ederler:

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

Diğer efektler temizleme aşamasına sahip olmayabilir ve hiçbir şey return etmezler.

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

Efekt Hook, her iki kullanım durumunu da tek bir API ile birleştirir.


Efektleri Kullanma İpuçları

Bu kısımda, deneyimli React kullanıcılarının muhtemelen merak edeceği useEffectin bazı yönlerine derinlemesine bakacağız.

İpucu: Endişeleri Ayırmak İçin Birden Çok Efekt Kullanma

One of the problems we outlined in the Motivation for Hooks is that class lifecycle methods often contain unrelated logic, but related logic gets broken up into several methods. Here is a component that combines the counter and the friend status indicator logic from the previous examples:

Hooks motivasyonunda ana hatlarıyla anlattığımız sorunlardan biri, class lifecycle fonksiyonlarının çoğu zaman ilgisiz bir mantık içermesi, ancak ilgili mantığın çeşitli yöntemlere bölünmesidir. Sayaç ile arkadaş durumu gösterge mantığını önceki örneklerden birleştiren bir component:

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `${this.state.count} kez tıkladın`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `${this.state.count} kez tıkladın`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

Yukarıda görüldüğü gibi, aynı kodlar gereksiz yere tekrar etmiştir.

Peki, Hook bu sorunu nasıl çözebilir? State hook’u bir kereden fazla kullanabildiğiniz gibi, çeşitli efektler de kullanabilirsiniz. Bu, ilgisiz mantığı farklı efektlere ayırmamızı sağlar:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `${this.state.count} kez tıkladın`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}

Hook, lifecycke fonksiyon isimlerinden ziyade, yaptıklarına göre kodu bölmemize olanak sağlar. React, component tarafından kullanılan her efekti, belirtilen sırada uygular.

Açıklama: Neden Her Güncellemede Efektler Çalışıyor?

Classlara alışkınsanız, efekt temizleme aşamasının neden her bir render işleminden sonra gerçekleştiğini değil, unmounting işlemi sırasında yalnızca bir kez olduğunu merak ediyor olabilirsiniz. Bu tasarımın neden daha az hata içeren component oluşturmamıza yardımcı olduğunu görmek için pratik bir örneğe bakalım.

Bu sayfada daha önce bir arkadaşın çevrimiçi olup olmadığını gösteren bir FriendStatus componenti yazıldı. Sınıfımız, this.props friend.id okur, component mounts edildikten sonra arkadaş durumuna abone olur ve unmounting işlemi sırasında aboneliği kaldırır:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

Ancak, component ekranda iken friend propsu değişirse ne olur? Componentimiz, farklı bir arkadaşın çevrimiçi durumunu göstermeye devam edecek. Bu bir hatadır. Abonelikten çıkma çağrısının yanlış arkadaş kimliğini kullanması nedeniyle bağlantıyı keserken de bellek sızıntısına veya çökmesine neden oluruz.

Bir class componentinde, bu durumu ele almak için componentDidUpdate eklememiz gerekir:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // Önceki friend.id aboneliğinden çık
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Sonraki friend.id aboneliğini başlat
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

ComponentDidUpdate kodunu doğru bir şekilde işlemeyi unutmak, React uygulamalarındaki yaygın bir hata kaynağıdır.

Şimdi bu componenti Hooks kullanan sürümünü düşünün:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

Bu bug muzdarip değil. (Ama aynı zamanda hiçbir değişiklik yapmadık.)

Güncelleştirmeler için özel bir kod yoktur çünkü useEffect bunları varsayılan olarak kullanır. Bir sonraki efektleri uygulamadan önce önceki efektleri temizler. Bunu göstermek için, işte bu componenti zaman içinde üretebileceği abone olma ve abonelikten çıkma çağrıları dizisini inceleyelim:

// { friend: { id: 100 } } propsu ile mount
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // İlk efekt çalışır

// { friend: { id: 200 } } propsu ile güncelleme
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Önceki efekt temizlenir
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Sonraki efekti çalıştır

// { friend: { id: 300 } } propsu ile güncelleme
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Önceki efekt temizlenir
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Sonraki efekti çalıştır

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Son efekti temizle

Bu davranış varsayılan olarak tutarlılığı sağlar ve eksik güncelleme mantığı nedeniyle class componentlerinde yaygın olan hataları önler.

İpucu: Efektleri Atlayarak Performansı İyileştirme

Bazı durumlarda her render işleminden sonra efekti temizlemek veya uygulamak performans sorunu oluşturabilir. Class componentlerinde bunu componenetDidUpdate içinde prevProps veya prevState ile fazladan bir karşılaştırma yaparak çözebiliriz:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `${this.state.count} kez tıkladın`;
  }
}

Bu gereksinim, useEffect Hook API’sine yerleşik olması için yeterince yaygındır. Yeni renderlar arasında belirli değerler değişmemişse React’a atla (skip) komutunu verebilirsiniz. Bunu yapmak için bir arrayi useEffect seçeneğine isteğe bağlı ikinci bir argüman olarak iletin:

useEffect(() => {
  document.title = `${count} kez tıkladın`;
}, [count]); // Yalnızca sayı değiştiğinde efekti yeniden çalıştır

Yukarıdaki örnekte, ikinci argüman olarak [count] yazıyoruz. Bu ne anlama geliyor? Eğer count hala 5’e eşit olan count ile yeniden renderlanırsa, React önceki renderdan [5] ve bir sonraki [5]‘i karşılaştırır. Arraydeki tüm elemanlar aynı olduğundan (5 === 5) React efekti atlar. Böylece performansa olumlu etki etmiş oluruz.

count 6 ile güncellendiğinde, React değişimi farkedecek (5 !== 6). Arrayde birden fazla öğe varsa, React biri farklı olsa bile efekti yeniden render eder.

Bu ayrıca temizleme aşaması olan efektler içinde çalışır:

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Yalnızca props.friend.id değişirse yeniden abone ol

Gelecekte, ikinci argüman otomatik olarak eklenebilir.

Gelişmiş kılavuzlar sonlanmıştır. Bu aşamadan sonra Uygulamalı Eğitime geçiş yapılacaktır.

Sıradaki Eğitim: XOX Oyunu