React Hooks ile Animasyon

Erdem Uslu
Jun 21 · 6 min read

Hooks ile ‘state’ yönetiminin nasıl yapılacağına dair ufak bir yazı hazırlamıştım. O vakitlerde hooks henüz yeni duyurulmuştu ve çok daha popüler olan veri yönetme tercihlerinin yanında henüz kullanılmaya hazır durmuyordu. Daha da doğrusu, hooks, kendisiyle başlanılacak daha ufak ve test yapmaya daha açık projelere muhtaçtı. O vakitten bu yana hooks ile çeşitli tecrübelerim oldu. Bunların çoğu çok ufak işlerdi ama sağladığı kolaylık ve ek bir pakete ihtiyaç duymama gibi durumlar oldukça tatmin edici sonuçlar doğurdu.

Motivasyon

Görselde görüldüğü gibi bir animasyon birçok şekilde yapılabilir. Hatta az biraz css kullanılarak dahi buna benzer bir animasyon yapmak mümkün. Burada amaçlanan ise; birden fazla ‘component’ınız var ve bunların birbirini takip ederek ekrana gelmesini istiyorsunuz. Ekranda asılı kalması gereken component oraya geldiğinde ise, o an kendisiyle alakalı bir işlemi olmayan component’ın dom’dan yok olmasını istiyorsunuz. Ve bunu yaparken de animasyonlu bir şekilde geçiş yapması gerekiyor.

İşte böylesi bir durumda, ki çoğu zaman karşılaşılması yüksek bir durum, hooks kullanarak kolaylıkla bir çözüm üretebilirsiniz. Burada anlatacağım mantığı, mobx ve styled-components ile kullanıyordum. Ancak tekrardan kullanmam gereken başka bir işte, hooks ile de fazladan bir paket kullanmadan yapabileceğimi farkettim.

Kaynak

Birazdan anlatacağım işin bitmiş haline github üzerinden ulaşabilirsiniz. Aynı repo’da burada anlattığım diğer derslerin kaynak kodu da var. Ayrıca canlı halini görmek için de buraya bakabilirsiniz.

Başlangıç

Projeyi daha önceki anlatımlarda olduğu gibi parcel ile ayaklandıracağız.

Kök dizinde varolan index.html dosyası src/main.js’ı çağırıyor ve sonrasında komut satırından parcel index.html diyerek projeyi başlatabiliyoruz.

Dizin yapısında actions’lar ile belirli etkileri reducer’lar üzerinden ayrıştırarak Store’a ulaştırıyoruz. Bu, tipini belirlediğimiz action’ların reducer içerisinde kontrol edilip çalıştırılması anlamına geliyor.

Bir adet reducer var ve onun içerisinde action kontrolü yapıyoruz. Store içerisinde ise component’ların index’ini ve style objesini tutuyoruz.

Burada dikkat edilmesi gereken nokta, data klasörü ve içerisindeki componentsData.jsx dosyası. Bu dosya içerisinde hazırlayacağımız animasyon döngüsü içerisinde dinamik olarak hareket etmesini sağlayacağımız component’ları tutuyoruz.

import React from 'react';

// load components
import Welcome from '../components/Welcome';
import Article from '../components/Article';
import Information from '../components/Information';
import End from '../components/End';

const componentsData = [
<Welcome />, <Article />, <Information />, <End />
];


export default componentsData;

Eğer ki akışımız içerisinde yeni bir component’a ya da arayüze ihtiyacımız var ise ve bu arayüzü bu akışa eklemlemek istiyorsak, yukarıdaki componentsData array’ine eklememiz yeterli olacaktır. Bu, sonrasında içerisinden Store’da tuttuğumuz index değeri ile çekeceğimiz aktif component’i belirlememizi kolaylaştıracak olan array.

Component Dizilimi

Burada klasik olarak bir App.js’imiz mevcut. App.js öncelikle Store elementini alıp, sonrasında components klasöründen Wrapper’ı alıp, Wrapper’ı Store’a child olarak veriyor. Wrapper ise kendi içerisinde animasyonu kotaracak olan bileşenleri ‘render’lıyor.

import React from 'react'

// load store
import Store from './store/Store';

// load components
import Wrapper from './components/Wrapper';

function App() {
return (
<Store>
<div className="container" role="main">
<Wrapper />
</div>
</Store>

)
}

export default App;

Böylelikle uygulamamız içerisinde Store içerisindeki state’leri ve o state’leri güncelleyebilme yetilerini alttaki component’lara dağıtmış olduk.

Şimdi ise Wrapper.js içerisinde Store içerisinde tuttuğumuz index değerine ait olan aktif component’i çağıracak bir yapı oluşturmalıyız. Bu işin en kolay kısmına denk geliyor.

import React, { useContext } from 'react'

// load main context
import { MainContext } from '../store/Store';

// load data
import componentsData from '../data/componentsData';

function Wrapper() {
const { state } = useContext(MainContext);

return (
<div role="main" className="wrapper">
{ componentsData[state.index] }
</div>

)
}

export default Wrapper;

Öncelikle MainContext’i Store’dan çağırıyoruz. Bu oluşturduğumuz state’i aşağıda, component’ın render’landığı yerde kullanabileceğimiz anlamına geliyor. Daha öncesinde oluşturduğumuz componentsData array’inden gerekli olan component’i çağırabilmek için, state’den eriştiğimiz index değeriyle, söz konusu array’den o değere denk düşen component’i render’lıyoruz.

Böylelikle, state.index değeri her değiştiğinde, sayfada varolan aktif component da değişecektir. Şimdi yapmamız gereken ise, söz konusu component’ların app’in yaşam döngüsünden çıkarken ya da girerken, bu işlemi animasyonla yapmaları. Bunun için ortak kullanıma açık bir Button component’ı oluştaracağız.

Store içerisindeki state’i güncellemek

Component’ların, çağırıldıkları array içerisinden dinamik olarak gelmesi için ortak bir component kullanarak söz konusu index’i artırmalıyız. bu işlemi components/shared klasörü altında oluşturacağımız Button component’i ile çözeceğiz.

import React, { useContext } from 'react';
import { string } from 'prop-types';

// load main context
import { MainContext } from '../../store/Store';

// load action
import updateIndex from '../../actions/updateIndex';
import { resetStyle, outStyle, inStyle } from '../../actions/updateStyle';

// load data
import componentsData from '../../data/componentsData';

function Button({ text }) {
const { state, dispatch } = useContext(MainContext);

// handle animation
const handle = () => {
// first animation
dispatch(outStyle());

// second animation
setTimeout(() => {
dispatch(updateIndex(state.index + 1))

// reset style
dispatch(resetStyle());

setTimeout(() => {
dispatch(inStyle());
}, 10);
}, 320);
}


return (
<button
type="button"
onClick={componentsData.length <= state.index + 1 ? null : handle}
>
{ text }
</button>
)
}

Button.defaultProps = {
text: ''
}

Button.propTypes = {
text: string
}

export default Button;

Tek tek üzerinden geçelim. MainContext’i, action’ları ve componentsData’yı çağırdığımız gözüküyor. MainContext’i state’e erişmek, action’ları state’i değiştirmek ve componentsData’yı ise length’ine erişerek, son aksiyonda olup olmadığımız kontrol etmek için çağırdık. Çağırdığımız 4 adet action’dan style ile ilgili olanlar animasyonu başlatıp, durdurmak için, ‘updateIndex’ ise adından da anlaşılacağı üzere index değerini güncellemek için.

Button’un text adında bir props’u var ve çağırıldığı yerde istendiği gibi değiştirilebilmesine olanak veriyor.

const { state, dispatch } = useContext(MainContext);

Dispatch ile reducer’lara hangi action’ı gönderdiğimizi bildirebileceğiz. Bu basit component’da sadece onClick üzerinden tetiklenen bir fonksiyon var. O da array’in içerisindeki son aksiyonda olup olmadığımıza göre tetikleniyor. Böylelikle son component’da animasyonun çalışmasını engellemiş ve boş bir ekranla karşılaşmamış olacağız.

Burada dikkat çeken nokta handle fonksiyonu içerisinde kurulan mantık. Burada style dosyaları içerisinde tanımladığımız bir nitelikten dolayı belirlenen bazı değerler var. Öncelikle style dosyasında ilgili parçacığa bakalım.

.box
...
transform: translateX(100%)
opacity: 0
transition: transform .32s linear 0s, opacity .32s linear 0s

Style içerisinde box adında bir class var ve bu animasyon döngüsünde varolan tüm component’larda tanımlanmış durumda. Burada iki değere transition değeri atanmış. ‘transform’ ve ‘opacity’. Dolayısıyla, .32s süresince gerçekleşecek olan bir transition söz konusu. Yani bu box class’ını barındıran component’lar eğer ki ‘transform’ ve ‘opacity’ style’lerinde bir değişikliğe uğrarlarsa, .32s’lik bir animasyon döngüsü içerisinde yapacaklar bu işlemi.

Bu bilgiyi göz önünde tutarak handle fonksiyonunda şöyle bir mantık güdüyoruz.

dispatch(outStyle());

Öncelikle aktif olan component’ın, yani o an array’den belirli bir index değeriyle çekilen component’ın, yine Store’dan çektiği style değerini, bu kısma aşağıda değineceğiz, güncelliyoruz. Bu da söz konusu component’ın animasyon döngüsüne girmesini sağlıyor.

Şimdi elimizde, ekrandan animasyon ile kaybolmuş bir component var. Ancak bu component hala ekranda render’lanıyor ve sadece style olarak yok hissiyatında. setTimeout ile 320ms sonra, yani style’de tanımlanan transition-duration süresi tamamlandıktan sonra, Store’daki index değerini güncelliyoruz.

dispatch(updateIndex(state.index + 1))

// reset style
dispatch(resetStyle());

Böylelikle aktif component’imiz bir sonrakine geçmiş oluyor. Ancak hemen peşisıra çalıştırdığımız bir action daha var. resetStyle. Burada şunu yapmak istiyoruz. Evet, aktif component’i değiştirdik ancak, ekrana gelirse eğer, güncellenmiş style değeriyle gelecek. Dolayısıyla style’ı tekrardan reset’liyerek varsayılan haline getiriyoruz. 10ms sonra da, aktif olan component’in ekrana animasyonlu bir şekilde girebilmesi için üçüncü action’u tetikliyoruz.

setTimeout(() => {
dispatch(inStyle());
}, 10);

Şu an aktif olan component’in yaşam döngüsüne animasyonla girmesini sağladık. Ortak kullanıma açık olan buton’dan handle fonksiyonu tetiklenecek olursa, hazırladığımız array içerisindeki component’lar belirli bir sıra halinde çıkış ve giriş animasyonu göstererek hareket edeceklerdir.

Array içerisindeki Component’lar

Bu ders için dört adet component hazırladık. İstenirse bunun sayısı çoğaltılabilir ya da kendi içlerindeki aksiyonlar genişletilebilir. Bu işlemler kurulan animasyon yapısıyla çakışmayacaktır. Bir sonraki component’ı animasyonlu bir şekilde çağırmak için aktif component içerisinde ilgili handle fonksiyonun çağırılması yeterli olacaktır.

import React, { useContext } from 'react';

// load main context
import { MainContext } from '../store/Store';

// load components
import Button from './shared/Button';

function Information() {
const { state } = useContext(MainContext);

return (
<div
role="main"
className="box information"
style={state.style}
>
Information
<Button
text="Call to action"
/>
</div>
)
}

export default Information;

Yukarıdaki örnek bir component. İçerisinde sadece basit bir metin ve Button component’ı var. Diğer üç component da aynı bu şekilde. Store’dan çekilen style objesi, animasyonun tetiklendiğinde bu component’lar üzerinde çalışmasını sağlıyor. Kendi isimleriyle class eklenmelerinin sebebi ise sadece basit bir arkaplan rengi vererek görselleştirmek.

Sonuç

Burada animasyonun React ve hooks geliştirmesiyle nasıl kotarılabileceği anlatılmış olsa da benzer mantığı kurgulayarak; Vue, Angular ya da ‘state management’ yaptığınız herhangi bir yapıda kolaylıkla uygulayabilirsiniz. Özellikle; form, anket ya da kullanıcı etkileşimine ihtiyaç duyulan alanlarda çokça karşılaşılan bir durum. Faydalı olması dileğiyle.