Detaylı React Hooks Kullanımı: useEffect
Published: 2023-04-05 14:16:32

`useEffect` hook'u, bileşenler içerisinde `side effects` işlemi yapmamıza olanak tanıyan bir hook'tur. Peki burada `side effects` ile ne kastediyoruz? React'te `side effects` DOM'u değiştirmek, network isteklerinde bulunmak veya global state'i güncellemek gibi bir bileşenin kapsamı dışındaki bir şeyi etkileyen herhangi bir davranış veya eylem anlamına gelir. `Side effects`, bir bileşenin render aşaması sırasında gerçekleştirilemeyen, bunun yerine bileşen render edildikten sonra yürütülmesi gereken işlemlerdir.

Bir API'dan veri çekmek buna bir örnektir. Diyelim ki bir API'dan bazı verileri almak ve componentinizde görüntülemek istiyorsunuz. Bir network isteğinde bulunmak, yani request atmak componentin kapsamı dışında bir işlem içerdiği için esasen bir `side effect`tir. Bu eylemi gerçekleştirmek için genellikle `useEffect` hook'u kullanılır:

```js

import React, { useState, useEffect } from 'react';

function FetchDataComponent() {
  const [data, setData] = useState([]);

  useEffect(() => {
    fetch('https://example.com/api/data')
      .then(response => response.json())
      .then(data => setData(data))
      .catch(error => console.error(error));
  }, []);

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
}


```

Yukarıdaki örnekte, bir requst atmak ve componentin state'ini alınan verilerle güncellemek için `useEffect` hook'unu kullanıyoruz. `useEffect`'e ikinci argüman olarak boş bir bağımlılık array'i ([]) geçerek, `useEffect`'in component monte edildikten sonra yalnızca bir kez yürütülmesini sağlıyoruz. Eğer `useEffect` kullanmasaydık, request atma her render işleminde yapılırdı, ki bu verimsiz olurdu ve performans sorunlarına yol açabilirdi.

Diğer `side effects` örnekleri arasında tarayıcı geçmişini manipüle etmek, window nesnesine erişmek veya harici bir olaya abone olmak/bir olayı takip etmek/ (*subscribe*) yer alır. `useEffect` hook'u, componentlerinizde bu işlemleri güvenli ve verimli bir şekilde gerçekleştirmenizi sağlar. Yeri gelmişken, harici (*external*) bir olaya abone olmanın ne olduğunu da bir örnek üzerinden anlatalım:

```js

import { useState, useEffect } from 'react';

function RealtimeDataDisplay() {
  const [data, setData] = useState([]);

  useEffect(() => {
    const interval = setInterval(() => {
      fetch('https://example.com/data-feed')
        .then(response => response.json())
        .then(newData => setData(data => [...data, newData]));
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return (
    <div>
      {data.map(item => (
        <p key={item.id}>{item.value}</p>
      ))}
    </div>
  );
}

```

Bu örnekte `useEffect` hook'u, `setInterval` metodunu kullanarak, her saniye https://example.com/data-feed URL'sine bir istekte bulunarak bir veri akışına abone olur. Veri alındığında, `setData` fonksiyonu kullanılarak componentin state'ine eklenir (yeni veriler varsa tabii...).

***

### `useEffect` Hook'unu Yalnızca İlk Render'da Çalıştırmak

`useEffect` hook'unun kullanımına geçmeden önce bu hook'un sentaksını, yani biçimini/formunu vermek iyi olacaktır. `useEffect` hook'u iki argüman alır (ikincisi opsiyoneldir):

```js

useEffect(<function>, <dependency>)

```

Yani, `useEffect` hook'undan sonra bir fonksiyon çağırırız ve bu `useEffect`'in çalışması da bir şeye bağlanır (*dependency*). Buradaki `dependency` array biçiminde gösterilir. Eğer array boş ise, hook yalnızca ilk renderda çağrılır. Yani,

```js

useEffect(() => {
  console.log('Hello');
}, []);

```

Yukarıdaki örnekte, `useEffect` hook'u sadece bir kez çalışır ve `Hello` yazdırır. Eğer `dependency` array'i boş değilse, `useEffect` hook'u, `dependency` array'inin içindeki herhangi bir değer değiştiğinde çalışır. Yani,

```js

useEffect(() => {
  console.log('Hello');
}, [name]);

```

Yukarıdaki örnekte, `useEffect` hook'u sadece (sözgelimi) `name` adındaki `state` değiştiğinde çalışır. Eğer `dependency` array'i verilmezse, `useEffect` hook'u yalnızca ilk renderda çalışır.

***

### `useEffect` Hook'unu Her Render'da Çalıştırmak

Yukarıdaki örneklerde `useEffect` hook'u yalnızca ilk renderda çalışıyordu ve daha sonrasında duruyordu - eğer `dependency` array boş ise. Peki biz `useEffect` hook'unu her render işlemi gerçekleştiğinde çalıştırmak istiyorsak ne yapmalıyız? Çok basit: `dependency` array'ini kaldıracağız! Yani,

```js

import React, { useState, useEffect } from 'react';

const App = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('I am called');
  });

  return (
    <>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </>
  )
}

export default App;

```

Yukarıdaki örnekte her butona tıklandığında `count` state'inin değeri artar ve `count` state'i yeniden render edilir. Bu yeniden render edilme işleminde `useEffect` hook'u devreye girer ve konsola `I am called` yazdırır.

***

Kaldığımız yerden devam edelim ve `useEffect` hook'unu daha detaylı bir şekilde ele alalım. Bunun için örneklerden yola çıkacağız.

Elimizde aşağıdaki gibi bir kod olsun:

```js

import React, { useState } from 'react'

const App = () => {

  const [section, setSection] = useState('posts');

  useEffect(() => {
  console.log('Hello');
}, []);

  return (
    <>
      <div>
        <button onClick={() => setSection('posts')}>Posts</button>
        <button onClick={() => setSection('users')}>Users</button>
        <button onClick={() => setSection('comments')}>Comments</button>
      </div>
      <h1> {section} </h1>
    </>
  )
}

export default App

```

Bu kodda, `section` adında bir `state` tanımlıyoruz ve bu state'i değiştirmek için üç tane buton tanımlıyoruz. Bu butonlara tıklandığında, `setSection` fonksiyonu ile `section` state'i değişiyor. Bu değişiklik, `section` state'inin değerini değiştiriyor ve bu da componentin tekrar render edilmesine neden oluyor. Fark edileceğiz üzere, `useEffect` hook'unun `dependency` array'ini boş bıraktık. Yani `useEffect` hook'u, component ilk kez çalıştırıldığında yürürlüğe girecektir ve konsola `Hello` yazdıracaktır. Butonlara tıklayıp `section` state'inin değerini değiştirsek bile, `useEffect` devreye girmeyecektir çünkü `useEffect` hook'unun `dependency` array'ini boş bıraktık. `useEffect` hook'undan her `section` state'inin değiştiği durumda konsola `Section state'i değişti!` yazdırmasını isteyelim:

```js

useEffect(() => {
  console.log("Section state'i değişti!");
}, [section]);

```

Bu şekilde, `useEffect` hook'u sadece `section` state'i değiştiğinde çalışır. Her `section` state'i değiştiğinde, meselâ butona tıklandığında ve `section` state'i `'users'` değeri aldığında, `useEffect` hook'u devreye girer ve konsola `Section state'i değişti!` yazdırır. `section` state'inin `initial state`'ini, yani `başlangıç hâli/durumu`'nu `posts` olarak belirlemiştik. Eğer ki `Posts` butonuna basarsak, konsola herhangi bir yazının yazılmadığını görürüz çünkü `useEffect` hook'u, `dependency` olarak `section` state'ini aldığı için, `section` state'i değişmediği sürece çalışmaz (çünkü state hâlâ `initial state` ile aynıdır). Ancak diyelim ki `Users` butonuna basarsak, konsola `Section state'i değişti!` yazısı konsola yazılır çünkü `section` state'i `initial state`'den farklı bir değere (yani, `users` değerine) sahip olmuştur.

***

Real-world scenario üzerinden bir örnek vermemiz gerekirse, bir {JSON}Placeholder API bulup onun üzerinden `useEffect` hook'unu ele alalım. Biz örnek için https://jsonplaceholder.typicode.com/ adresindeki endpointleri kullanacağız. Aşağıdaki kodu inceleyelim:

```js

import React, { useState, useEffect } from 'react';

const App = () => {
  const [section, setSection] = useState('posts');

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/${section}`)
      .then((response) => response.json())
      .then((json) => console.log(json));
  }, []);

  return (
    <>
      <div>
        <button onClick={() => setSection('posts')}>Posts</button>
        <button onClick={() => setSection('users')}>Users</button>
        <button onClick={() => setSection('comments')}>Comments</button>
      </div>
      <h1> {section} </h1>
    </>
  );
};

export default App;

```

Yukarıdaki örnekte API endpointine `section` state'inin aldığı değere göre GET isteğinde bulunan bir fetch, yani veri çekme işlemi yaptık. `useEffect` hook'unun `dependency` array'ini ise boş bıraktık. Bu, şu anlama geliyor: Sayfa ilk kez render edildiğinde API'dan verileri çek ve daha sonra bir şey yapma. Biz meselâ `Users` veya `Comments` butonuna tıklasaydık, `section` state'i değişmesine rağmen API'a attığımız istek değişmeyecekti ve konsoldaki JSON verilerinde bir güncelleme, değişme, ekleme vb. olmayacaktı. Neden? Çünkü `useEffect` hook'unun `dependency` array'i `section` state'inin değerine bağlı değildir. `dependency` array'i `section` state'i olarak belirlediğimizi düşünelim:

```js

import React, { useState, useEffect } from 'react';

const App = () => {
  const [section, setSection] = useState('posts');

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/${section}`)
      .then((response) => response.json())
      .then((json) => console.log(json));
  }, [section]);

  return (
    <>
      <div>
        <button onClick={() => setSection('posts')}>Posts</button>
        <button onClick={() => setSection('users')}>Users</button>
        <button onClick={() => setSection('comments')}>Comments</button>
      </div>
      <h1> {section} </h1>
    </>
  );
};

export default App;

```

Aslında gerçekleşecek işlemleri tahmin etmek çok basit:

1. `section` state'inin `initial state`'i `posts` olarak belirlendiğinden, ilk render edildiğinde `useEffect` hook'u çalışır ve API'dan `posts` verilerini çeker. Yani, https://jsonplaceholder.typicode.com/**posts** adresine bir GET isteği atar ve gelen verileri JSON formatına çevirerek konsola yazdırır.

2. `Posts` dışındaki bir butona tıklandığında, meselâ `Users` butonuna tıklandığında `section` state'i `users` değerini alır ve `useEffect`'in sahip olduğu `dependency` array'i tetiklenerek, `useEffect` hook'unu devreye sokar. `section` artık `users` değeri aldığı için, `useEffect` hook'u https://jsonplaceholder.typicode.com/**users** adresine bir GET isteği atar ve bu endpoint'in bize sağladığı verileri JSON formatına çevirerek konsola yazdırır.

3. İkinci adımdaki işlemin aynısı `Comments` butonuna tıklandığında da gerçekleşir.

Konsola yazdırmak yerine aynı işlemi yaparak gelen verileri ekrana yazdıralım. Bunun için gelen verileri bir array içerisinde tutan bir state'e ihtiyacımız olacaktır. Yani, aşağıdaki kod parçasını inceleyelim:

```js

import React, { useState, useEffect } from 'react';

const App = () => {
  const [section, setSection] = useState('posts');
  const [items, setItems] = useState([]);

  useEffect(() => {
    fetch(`https://jsonplaceholder.typicode.com/${section}`)
      .then((response) => response.json())
      .then((json) => setItems(json));
  }, [section]);

  return (
    <>
      <div>
        <button onClick={() => setSection('posts')}>Posts</button>
        <button onClick={() => setSection('users')}>Users</button>
        <button onClick={() => setSection('comments')}>Comments</button>
      </div>
      <h1> {section} </h1>
      <h2>Çekilen Veriler</h2>
      {items.map((item, i) => (
        <pre key={i}>{JSON.stringify(item, null, 2)}</pre>
      ))}
    </>
  );
};

export default App;

```

1. İlk olarak `items` adında array alan bir state oluşturduk ve gelen verileri `items` array'inde depolamak için `fetch` işleminin içerisinde gelen verileri `setItems` fonksiyonuyla JSON'a çevirerek `items` array'ine depoladık.

2. Daha sonra `items` array'ini mapleyerek ekrana yazdırdık.

3. `dependency` array'inde `section` state'i olduğu için, `section` state'i değiştiğinde `useEffect` hook'u çalışır ve API'dan verileri çeker. Bu sayede, `Posts` butonuna tıkladığımızda `posts` verilerini, `Users` butonuna tıkladığımızda `users` verilerini, `Comments` butonuna tıkladığımızda `comments` verilerini çekmiş oluruz. Çekilen verileri de son olarak `map` ile ekrana yazdırırız.

***

Başka bir örnek daha yapalım. Anlık pencere genişliğini veren bir state tanımlayalım ve bu state'in değerini de ekrana yazdıralım:

```js

import React, { useState } from 'react';

const App = () => {
  const [windowWidth, setWindoWidth] = useState(window.innerWidth);

  return (
    <>
      {windowWidth}
    </>
  );
};

export default App;

```

1. İlk olarak `windowWidth` adında bir state tanımladık ve bu state'in `initial state`'ini pencerenin iç genişliği (*window.innerWidth*) olarak belirledik.

2. Daha sonra da `windowWidth` state'inin aldığı değeri ekrana yazdırdık.

Benim ekranım full olduğu için ekranda `1440` değerini görüyorum. Ancak bu statik bir değer. Browserımın genişliğini biraz küçültüp sayfayı yenilersem, bu değer farklı olacaktır. Ancak bunu sayfayı yenilemeden, dinamik bir şekilde yapabilir miyiz?

Aşağıdaki kodu inceleyelim:

```js

import React, { useState, useEffect } from 'react';

const App = () => {
  const [windowWidth, setWindoWidth] = useState(window.innerWidth);

  const handleResize = () => {
    setWindoWidth(window.innerWidth);
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize);
  }, []);

  return (
    <>
      {windowWidth}
    </>
  );
};

export default App;

```

Burada `useEffect` hook'unu işin içerisine dahil edip bir `EventListener` ekliyoruz ve ilk argüman olarak event türünü (buradaki `resize`), ikinci argüman olarak da bu event tetiklendiğinde uygulanacak olan fonksiyonu yazıyoruz (burada `handleResize` fonksiyonu). Bundan böyle her browser penceresinin genişliği değiştiğinde `handleResize` fonksiyonu çalışacak ve `windowWidth` state'i değişecek. Dolayısıyla da ekrana yazdırılan `windowWidth` değeri değişecek.

**NOT:** Performans sorunlarıyla karşılaşmamak için eklediğimiz bu `EventListener`'ı silmemiz iyi olacaktır:

```js

import React, { useState, useEffect } from 'react';

const App = () => {
  const [windowWidth, setWindoWidth] = useState(window.innerWidth);

  const handleResize = () => {
    setWindoWidth(window.innerWidth);
  };

  useEffect(() => {
    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    }
  }, []);

  return (
    <>
      {windowWidth}
    </>
  );
};

export default App;

```

Burada `useEffect` hook'unun içerisine bir `return` ifadesi ekledik ve bu `return` ifadesinin içerisinde `EventListener`'ı sildik. Bu sayede, `useEffect` hook'u çalıştırılmadan önce `EventListener`'ı ekliyoruz ve `useEffect` hook'u çalıştırıldıktan sonra da `EventListener`'ı siliyoruz.

Bunu yapmak önemlidir çünkü `EventListener` kaldırılmazsa, component unmount edildikten sonra bile değişiklikleri dinlemeye devam edecektir. Bu da uygulamanızda bellek sorunlarına ve beklenmedik davranışlara yol açabilir.

`EventListener`'ı kaldıran bir fonksiyon `return` ederek, componente artık ihtiyaç duyulmadığında `EventListener`'ın temizlenmesini sağlarsınız.

Özetle, bir `useEffect` hook'una bu tür durumlarda bir `removeEventListener` eklemek, bellek sorunlarını önlemek ve uygulamanızın sorunsuz çalışmasını sağlamak için en iyi uygulamadır.

`useEffect` içerisinde temizlik işlemleri yapmak isteyenler veya daha basit bir şekilde bu işlemin sentaksını, yani biçimini/formunu görmek isteyenler için aşağıya bu işlemin sentaksını ekliyorum:

```js

useEffect(() => {
  console.log('This is my side effect')

  return () => {
    console.log('This is my clean up')
  }
})

```