6- ReactJS Redux ile Global State Yönetimi

Bir önceki dersimizde layout yönetimi konusunu ele almıştık.

Serimizin altıncısı ile devam ediyoruz.

Eğitim Künyesi

Global State Nedir?

Üçüncü dersimizde local state’in yönetiminden bahsetmiştik.

Kısaca tekrarlayacak olursak, local state değerlerinin değişikliği içinde tanımlandığı componentin render olmasını sağlıyordu.

Global state ise, component ağacının herhangi bir nodunda bulunan bir compnente gerçekleşen bir değişikliğin yine component ağacının başka herhangi bir componentine yansıyıp o componentin render olmasını istiyorsak bu işi global state mekanizması ile çözüyoruz.

Bu durumu bizim örneğimiz ile açıklayalım. Hatırlarsanız yine üçüncü dersimizde Header componentini her sayfaya import edip title propunu dışardan gönderip dinamik bir şekilde her sayfa için içerisinde farklı bir metin görüntülenmesini sağlamıştık.

Beşinci dersimizde ise Header componentini sadece layout içinde import edip yine bütün sayfalarda görüntülenmesini sağlamıştık. Ancak bu sefer de sayfa ile Header componenti arasına bir kaç component daha girmiş ve aralarında parent child ilişlisi uzaklaşmıştı. Bu gibi durumlarda props ile iki parent child veri ilişkisi kurmak gittikçe zorlaşır.

Bu durumu aşağıdaki görseller ile anlatmaya çalışalım.

Header ve sayfa componentlerimizin arasındaki ilk ilişki bu şekildeydi. Dolayısıyla hızlı ve kolay bir şekilde Header componentime bilgi aktarabiliyordum.

Component parent child relation
Sayfa ve Header Componenti Arasındaki İlk İlişki

Layout revizyonlarımızla birlikte componentler arası ilişki aşağıdaki şekilde olmaya başladı. Header ve sayfa componentlerim (sayfalar children propuna göre değişken olacak) layout içinde aynı seviyede bulunuyor. Dolayısıyla aralarında bir parent child ilişkisi kalmadı.

Node Tree
Layout Değişikliği Sonrası Compnent Ağacı

Bir de ağaç görseli üzerinde “x” diye bahsettiğim, Header componentinin alt seviylerinde olan ancak child’ı olmayan başka bir olası component yer alıyor. İşte bu complex component ağacında ben, x componentinden ya da page componentlerinden birinden bir tetikleme ile Header componenti içinde yer alan değeri değiştirebiliyorsam bunu global state mekanizmaları sayesinde yapıyorum.

React Global State
Global State Mekanizması

Yukarıdaki şemada tüm componentlerin üstünde Global State yer alıyor. Herhangi bir konumdan sarı çizgi ile Global State içinde yer alan title değerini ise x ve UserList componentleri herhangi bir anda güncelliyor. Global State’e yeşil çizgi ile subscribe olan Header componenti title değerini buradan okuyor.

Redux Nedir? Redux’ın Bileşenleri

Redux, global state’i yönetmemize yarayan üçüncü parti bir kütüphanedir. İlk, 2015 yılında Dan Abramov ve Andrew Clark tarafından geliştirilip açık kaynak olarak sunulmuştur. Uygulama içindeki veri akışını yöneterek daha kontrollü hale getirilmesini sağlar. React’ten bağımsız tüm Javascript uygulamalarında kullanılabilir. React tarafında işleri kolaylaştırmak için aracı kütüphaneleri vardır.

Redux’a alternatif olarak React tarafından sunulan Context API kullanılabilir ancak bazı otoriteler, complex veri yapısına sahip ve sıkça güncellenen state yapıları için Context’i önermiyor. Context’in, Redux gibi boiler plate kod yazma işinden kurtardığı düşünülse de complex veri setlerinin güncellenebilmesi için hooksla gelen useReducer yapısının kullanılması gerekir. İşin içine reducer girince daha sonradan da bahsedeceğimiz action dispatch etme, action const hazırlama ve reducer yazma gibi işlemleri de yapmamız gerekecektir 🙂

Complex olmayan, küçük ölçekli uygulamalar için Context API kullanılabilir. Ancak biz eğitimimize Redux ile devam edeceğiz.

Redux, ilk aşamada karmaşık gelse de içeriğini oluşturan parçaları ve veri akışını anladıktan sonra anlaşılması kolaylaşacaktır.

Redux taradından yönetilen global state’imiz (store), direk olarak değiştirilemez. Kontrollü akışı sağlamak ve dinleyen componentleri render etmek için belli aşamalar gerçekleştikten sonra güncellenebilir. Bu aşamaları aşağıdaki görsel ile anlatmaya çalışalım.

What is Redux? How to Use It?. Redux is a simple tool for state ...
Redux Veri Akışı

Yukarıdaki görseli bileşenlerin tam olarak ne olduğunu açıklamadan biraz inceleyelim.

Kullanıcı UI tarafında bir event vasıtasıyla action fonksiyon çağırır. Reducer, action fonksiyonun çağırıldığını algılar ve action fonksiyon tarafından bildirilmiş alan sayesinde store’daki ilgili alanı günceller. Şimdi bahsi geçen bileşenleri tek tek açıklayalım.

  • UI: Kullanıcının gördüğü herhangi bir ekran componenti yani input alabileceğimiz herhangi bir UI parçası.
  • Action: UI tarafında tetiklenen fonksiyonlardır. Herhangi bir veri alıp dönüş olarak içerisinde type alanı olan bir obje dönmek zorundadır({type: “SET_TITLE”}). Reducer’a veri taşır. Örneğin bir sayfayı açtıktan sonra ya da bir butona tıklandıktan sonra bir action fonksiyonu çağırıp verinin bir sonraki aşamaya taşınmasını sağlayabiliriz.(dispatch)
  • Reducer: Global state(store) üzerindeki değişikliklerden sorumludur. Action’lar tarafından iletilen bilgi ile içerisinde değişikliğe uğrayacak alanın hangisi olduğuna karar verir ve güncel state verisini eskisinin yerine günceller. Pure bir Javascript fonksiyonudur.
  • Store: Verilerimizin bulunduğu alandır. Direkt güncellenemez. Sadece reducer vasıtasıyla güncellenebilir.

Redux hakında alternatif video anlatımı için tıklayınız.

Kolları sıvayalım mı?

Redux’ın Projede Uygulanması

Redux hakkındaki anlatımlarımızdan sonra daha da pekişmesi açısından projemiz üzerinde uygulamaya başlayalım.

İlk olarak redux ve react-redux kütüphanelerini aşağıdaki komut ile projemize dahil edelim

npm i redux react-redux

redux: Mekanizmaya ait core mimariyi sağlıyor

react-redux: Redux mekanizmasının React tarafında ihtiyaç duyulan fonksiyonları sağlıyor. Örneğin storedan componente veri çekmek gibi.

Kurulumlar tamamlandıktan sonra kod yazmaya başlayalım.

Reducer’ın Tanımlanması

Örneğimizde Header componenti içerisinde string bir title değeri gösterecemizden bahsetmiştik. Şimdi bu değeri store’umuzda barındırabiliriz. Bunun için src klasörü altında state isimli bir klasör oluşturup onun da içine reducers/AppReducer.js dizinli bir dosya oluşturalım ve içini aşağıdaki gibi dolduralım. Dizin yapımızı şu şekilde olacaktır.

const INITIAL_APP_STATE = {
    title: "First Screen"
};

export default (state = INITIAL_APP_STATE, action) => {
    switch (action.type) {
        case "SET_PAGE_TITLE":
            return {...state, title: action.payload}
    
        default:
            return state;
    }
}

İlk olarak içerisinde title propertisi olan bir başlangıç değeri oluşturuyoruz.

Sonrasında direk bu dosyadan export olan bir Javascript fonksiyonu oluşturuyoruz. Bu fonksiyon iki adet parametre alıyor. İlk parametre oluşturduğumuz state nesnesi içerisinde değiştirmek istediğimiz alanlar olacak. Şimdilik title bilgisini tutup başlangıç değeri veriyoruz.

Diğer parametre ise action. Daha önceden tanımını yaptığımız kullanıcı tarafından gönderilen fonksiyonlar. Bu fonksiyonun gönderimi redux mekanizması sayesinde yakalanıyor ve şuan tanımını yaptığımız reducer’a ikinci parametre olarak gönderiliyor. Action metodu zorunlu olarak {type: “ACTION_NAME”} olarak içinde type propu olan bir obje dönmek zorunda.

Gönderilen type bilgisi sayesinde reducer içinde switch case mekanizması işletiliyor ve zorunlu olmayan payload prop’u ile state’in title alanı güncellenip, güncel state objesi return ediliyor.

Return edilen yeni state objesi redux mekanizması tarafından algılanıyor ve global state’e yazılıyor.

Bu şekilde kod üzerinden bir kez daha reducer mimarisini anlatmış olduk.

Store’un Oluşturulması. combineReducers, createStore ve Provider Kullanımı

Şimdi de nihayi verilerin tutulacağı store’u oluşturalım Store’un oluşturulması için önceden reducer’ların oluşturulup store’a sunulabilir hale getirilmesi gerekiyor.

Sunulabilir olması için aşağıdaki dosyası src/state/reducers/index.js konumunda oluşturalım.

import { combineReducers } from 'redux'
import AppReducer from './AppReducer'

export default combineReducers({
    app: AppReducer
});

Burada ne yapıyoruz?

combineReducers isimli bir yardımcı fonksiyonu redux kütüphanesinden import ediyoruz.

Oluşturduğumuz AppReducer isimli reducer’ı import ediyoruz.

sonrasında combineReducers isimli fonksiyon yardımıyla AppReducer’dan dönen state değerini app isimli bir alana atayıp store’un kullanımına hazır hale getirmiş oluyoruz. Burda anlaşılacağı üzere birden fazla reducer farklı alanlara atanıp bir araya getirilebilir. Günün sonunda hepsin combineReducers vasıtasıyla derlenip store’a sunulmuş oluyor.

export default diyerek oluşan combinasyonu dışarırdan kullanıbilir hale getirmiş oluyoruz. Bu işlemden sonra src/state/store.js yoluna aşağıdaki şekilde store’umuzu oluşturalım.

import { createStore } from 'redux'
import reducers from './reducers';

export default createStore(reducers, {});

Burada da createStore yardımcı fonksiyonu ile store oluşturma işlemini gerçekleştirmiş oluyoruz.

import edilen reducers objesi biraz önce combineReducers ile oluşturduğumuz değerdir.

Artık store’umuz hazır. Oluşturulan store’un uygulamanın her yerinde kullanılabilir hale gelmesi için en üst node’umuzda kullanacağımız store’u Provider vasıtasıyla belirtmemiz gerekiyor.

Bunun için de App.js içerisinde aşağıdaki gibi değişiklik yapıyoruz.

import store from './state/store';

App fonksiyonun return kısmında:

function App() {

  return (
    <Provider store={store}>
      <div className="App">
        <Router>
          <Switch>
            
            <PublicRoute path="/" exact>
              <UserList />
            </PublicRoute>

            <PublicRoute path="/user-detail/:name" exact>
              <UserDetail />
            </PublicRoute>

          </Switch>
        </Router>
      </div>
    </Provider>
  );
}

Bu şekilde ağaç yapısını Provider ile sararak uygulama çapında store’un kullanılabilir olmasını sağlamış oluyoruz.

Store’da Yeralan Değerin Component İçinde Kullanımı. useSelector.

AppReducer içerisinde tanımladığımız ve başlangıç değeri verdiğimiz title alanını Header componenti içinde kullanalım.

Bunun için Header componenti içinde ilk önce useSelector yardımcı metodunu react-redux kütüphanesinden import ediyoruz.

import { useSelector } from 'react-redux';

Sonrasında component fonksiyonu içinde store’da yer alan title değerini useSelector yardımı ile alıp component içinde {title} ile kullanıyoruz.

const Header = (props) => {

    const title = useSelector(state => state.app.title);

    return (
      <header className="App-header">
          {title}
      </header>
    )
}
const title = useSelector(state => state.app.title);

useSelector, bize global state’i verir. state.app.title ile biz modellediğimiz state içerisinden ilgili alanı almış oluruz. İlgili alanı bu şekilde belirterek aslında bir yandan da title alanına subscribe olmuş oluyoruz.

Artık title değeri herhangi bir component’ten değiştirilirse Header componenti bu değeri dinlediği için tekrar render olup güncel değeri gösterecektir.

state.app.title verisini incelediğimizde, state, tüm verilerin bulunduğu ana obje. app, combineReducers fonksiyonu ile reducer’ları hazırlarken kullandığımız isim. title ise AppReducer içinde hazırladığımız alan oluyor.

İleride farklı bir veri kümesi oluşturmak istediğimde örneğin state.users.userList şeklinde bir veri uzantısına sahip olabilirdim.

Bu kısım tamamiyle veri yapısını nasıl tasarladığımızla alakalı.

Her şeyi doğru yaptıysak ekran çıktımız aşağıdaki gibi olacaktır.

Header kısmında yazan “First Screen” değeri reducer’da title’a verdiğimiz başlangıç değerinden geliyor.

Şimdi ise sayfa değiştiğinde title bilgisinin değişmesi konusunu inceleyelim.

Global State’de (Store) Yer Alan Veriyi Değiştirme. Action Dispatch

Buraya kadar store’umuzda yer alan bir bilgiyi tek yönlü bir şekilde componentimize kadar nasıl getirizi gördük.

Şimdi ise reducer’ımıza action göndererek (action dispacth etme) store’umuzda yer alan title değerinin değişmini sağlayıp component’imizin render olmasını sağlayacağız.

Bizim örneğimizden iki adet sayfa yer alıyor. Sayfalar kullanıcı tarafından ziyaret edildiğinde Header tarafında ilgili sayfanın başlığını yazılmasını istiyoruz.

İlk olarak UserList componentinde aşağıdaki gibi bir değişklik yapalım.

import { useDispatch } from 'react-redux';

Sonrasında fonksiyon içine aşağıdaki satırları ekleyelim

const dispatch = useDispatch();

React.useEffect(() => {
  dispatch({
    type: "SET_PAGE_TITLE",
    payload: "User List"
  });
}, []);

İlk olarak useDispatch hook’unu react-redux’tan import ettik. Bu hook action dispatch etmeye yardımcı oluyor.

Hook’lar genel kural olarak component içinde kullanılmalıdır. Tek başına çağrılması bir anlam ifade etmez. Dönüş değerine göre işlem yapılır.

Bizim örneğimizde useDispatch hook’u bize kullanılabilir bir dispatch metodu dönüyor. Biz de bu dispatch metodu ile action dispatch etmiş oluyoruz

React.useEffect metodu React’in temel hooklarından biri. Daha sonraki lifecycle dersinde detaylı anlatımı yapılacak ancak kısaca sayfa yüklendiğinde tetiklenen kısım olduğunu söyleyebiliriz.

Şimdi dispatch ile gönderdiğimiz objeyi ele alalım.

İçerisinde type ve payload olan iki adet property gönderiyoruz. Bunlardan type olanı zorunlu diye bahsetmiştik. Type alanı ile reducer içindeki hangi alanı güncelleyeceğimizi switch içinde case’e sokarak belirtiyoruz. Payload ile de title alanına hangi değeri atayacağımızı belirtiyoruz.

dispatch({
    type: "SET_PAGE_TITLE",
    payload: "User List"
 });
const INITIAL_APP_STATE = {
    title: "First Screen"
};

export default (state = INITIAL_APP_STATE, action) => {
    switch (action.type) {
        case "SET_PAGE_TITLE":
            return {...state, title: action.payload}
    
        default:
            return state;
    }
}

Gerekli açıklamalarımızı yaptığımıza göre aynı işlemi UserDetail için de gerçekleştirelim.

import React from 'react';
import { users } from '../../data';
import { useParams } from 'react-router-dom';
import { useDispatch } from 'react-redux';

const UserDetail = () => {

    const { name } = useParams();
    const user = users.filter(f => f.name === name)[0];

    const dispatch = useDispatch();
    React.useEffect(() => {
        dispatch({
            type: "SET_PAGE_TITLE",
            payload: `User Detail ${user.name}`
        });
    }, []);

    return(
        <>
        <div className="user-detail">
            <b>Name: </b> {user.name} {user.surname} <br/>
            <b>Title: </b> {user.title} <br/>
            <b>City: </b> {user.city} <br/>
        </div>
        </>
    );
}

export default UserDetail;

Burada dikkat ederseniz payload bilgisiyle seçilen kişinin ismini de dinamik olarak dispatch ediyoruz.

dispatch({
  type: "SET_PAGE_TITLE",
  payload: `User Detail ${user.name}`
});

Tüm bu değişikliklerden sonra ekran çıktılarımız aşağıdaki gibi olacaktır.

Konuyu sonlandırmadan önce ufak bir sidebar stillendirmesi yapalım. app.scss dosyası içerisinde .side-bar altında aşağıdaki değişiklikleri yapalım.

.sidebar{
  flex: 2;
  margin-top: 8px;
  background-color: $header-bg-color;
  ul{
    border: none;
    a {
      color: white;
      text-decoration: none;
    }
  } 
}

Bu değişiklikle birlikte sidebar kısmı header ile aynı arka plana sahip olmuş olacak.

Bu işlem ile birlikte dersimizi sonlandırıyoruz. Derste yazdığımız kodların son haline buradan erişebilir, dilerseniz aşağıdan çalıştırılabilir haline gözlemleyebilirsiniz.

Bir sonraki dersimizde lifecycle konusunu ele aldık.

Sağlıkla kalın 🙂

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.