State Mantığını Bir Reducer'a Aktarma
Birçok olay yöneticisine yayılmış çok fazla sayıda state güncellemesine sahip bileşenler can sıkıcı olabilir. Bu gibi durumlarda tüm state güncelleme mantıklarını reducer (redüktör) adı verilen tek bir fonksiyonda birleştirebilirsiniz.
Bunları öğreneceksiniz
- Bir reducer fonsiyonunun ne olduğu
useState
’iuseReducer
ile nasıl yeniden yapılandıracağınız- Ne zaman bir reducer kullanmanız gerektiği
- İyi bir reducer yazmanın püf noktaları
State mantığını (State logic) bir reducer ile birleştirin
Bileşenlerinizin karmaşıklığı arttıkça bir bileşenin state’inin hangi farklı yollarla güncellendiğini bir bakışta görmek zorlaşabilir. Örneğin, aşağıdaki TaskApp
bileşeni görevler
dizinini bir state’de tutar ve görevleri eklemek, kaldırmak ve düzenlemek için üç farklı olay yöneticisi kullanır:
import { useState } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, setTasks] = useState(initialTasks); function handleAddTask(text) { setTasks([ ...tasks, { id: nextId++, text: text, done: false, }, ]); } function handleChangeTask(task) { setTasks( tasks.map((t) => { if (t.id === task.id) { return task; } else { return t; } }) ); } function handleDeleteTask(taskId) { setTasks(tasks.filter((t) => t.id !== taskId)); } return ( <> <h1>Prag Gezisi Planı</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Kafka Müzesini ziyaret et', done: true}, {id: 1, text: 'Kukla gösterisi izle', done: false}, {id: 2, text: "Lennon Duvarı'nda fotoğraf çek", done: false}, ];
Her bir olay yöneticisi state’i güncellemek için setTasks
’ı çağırır. Bu bileşen büyüdükçe, içine serpiştirilmiş state mantığı miktarı da artar. Bu karmaşıklığı azaltmak ve tüm mantığı erişilmesi kolay tek bir yerde tutmak için state mantıklarını bileşeninizin dışında “reducer” adı verilen tek bir fonksiyona taşıyabilirsiniz.
Reducer’lar, state’i ele almanın farklı bir yöntemidir. useState
’ten useReducer
’a şu üç adımda geçebilirsiniz:
- State ayarlamak yerine işlemleri göndermeye (dispatching) geçme.
- Bir reducer fonksiyonu yazma.
- Bileşeninizden gelen “reducer”ı kullanma.
Step 1: State ayarlamak yerine işlemleri göndermeye (dispatching) geçme
Olay yöneticileriniz şu aşamada ne yapılacağını state ayarlayarak belirler:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
Tüm state ayarlama mantığını kaldırın. Geriye üç olay yöneticisi kalacaktır:
handleAddTask(text)
kullanıcı “Ekle” butonuna bastığı zaman çağrılır.handleChangeTask(task)
kullanıcı bir görevi açıp kapattığında veya “Kaydet” butonuna bastığında çağrılır.handleDeleteTask(taskId)
kullanıcı “Sil” butonuna bastığında çağrılır.
Reducer’lar ile state yönetimi doğrudan state’i ayarlama işleminden biraz farklıdır. State ayarlayarak React’e “ne yapılacağını” belirtmek yerine, olay yöneticilerinden “işlemler” göndererek “kullanıcının ne yaptığını” belirtirsiniz. (State güncelleme mantığı başka bir yerde yaşayacaktır!) Yani bir olay yöneticisi aracılığıyla “görevleri ayarlamak” yerine, “görev eklendi/değiştirildi/silindi” şeklinde bir işlem gönderirsiniz. Bu kullanıcının isteğini daha açık hale getirir.
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task,
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId,
});
}
dispatch
’e gönderdiğiniz nesneye “işlem” adı verilir.
function handleDeleteTask(taskId) {
dispatch(
// "işlem" nesnesi:
{
type: 'deleted',
id: taskId,
}
);
}
Bu bildiğimiz bir JavaScript nesnesidir. İçine ne koyacağınıza siz karar verirsiniz, ancak genellikle ne meydana geldiği hakkında minimum bilgi içermelidir. (dispatch
fonksiyonunun kendisini daha sonraki bir adımda ekleyeceksiniz.)
Step 2: Bir reducer fonksiyonu yazma
Bir reducer fonksiyonu state mantığınızı (state logic) koyacağınız yerdir. İki argüman alır; mevcut state ve işlem nesnesi, ardından bir sonraki state’i geri döndürür:
function yourReducer(state, action) {
// React'in ayarlaması için bir sonraki state'i geri döndür
}
React reducer’dan ne geri döndürürseniz state’i ona göre ayarlayacaktır.
Bu örnekte state ayarlama mantığınızı olay yöneticilerinden bir reducer fonksiyonuna taşımak için şunları yapacaksınız:
- Geçerli state’i (
tasks
) ilk argüman olarak tanımlayın. action
nesnesini ikinci argüman olarak tanımlayın.- Reducer’dan (React’in state’i ayarlayacağı) bir sonraki state’i geri döndürün.
Burada tüm state ayarlama mantığı tek bir reducer fonksiyonuna aktarılmıştır:
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Bilinmeyen işlem: ' + action.type);
}
}
Reducer fonksiyonu state’i (tasks
) bir argüman olarak aldığından bunu bileşeninizin dışında tanımlayabilirsiniz. Bu satır girinti seviyesini azaltır ve kodunuzun okunmasını kolaylaştırır.
Derinlemesine İnceleme
Reducer’lar bileşeninizin içindeki kod miktarını “azaltabilir” (reduce) olsa da, aslında diziler üzerinde gerçekleştirebileceğiniz reduce()
metodundan almakta.
reduce()
işlemi bir diziyi alıp birçok değeri tek bir değerde “toplamanızı” sağlar:
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5
reduce
’a aktardığınız fonksiyon “reducer” olarak bilinir. Bu fonksiyon o ana kadarki sonucu ve geçerli öğeyi alır, ardından bir sonraki sonucu geri döndürür. React reducer’lar da aynı fikrin bir örneğidir: o ana kadarki state’i ve işlemi alırlar ve bir sonraki state’i geri döndürürler. Bu şekilde, işlemleri zaman içinde state olarak toplarlar.
Hatta reduce()
metodunu bir initialState
ve bir actions
dizisi ile kullanabilir ve reducer fonksiyonunuzu bu metoda aktararak son state’i hesaplayabilirsiniz:
import tasksReducer from './tasksReducer.js'; let initialState = []; let actions = [ {type: 'added', id: 1, text: 'Kafka Müzesini ziyaret et'}, {type: 'added', id: 2, text: 'Kukla gösterisi izle'}, {type: 'deleted', id: 1}, {type: 'added', id: 3, text: "Lennon Duvarı'nda fotoğraf çek"}, ]; let finalState = actions.reduce(tasksReducer, initialState); const output = document.getElementById('output'); output.textContent = JSON.stringify(finalState, null, 2);
Muhtemelen bunu kendiniz yapmanız gerekmeyecektir, ancak bu React’in yaptığına benzer!
Step 3: Bileşeninizden gelen “reducer”ı kullanma
En son olarak, tasksReducer
’ı bileşeninize bağlamanız gerekiyor. useReducer
Hook’unu React’ten içe aktarın:
import { useReducer } from 'react';
Daha sonra useState
’i:
const [tasks, setTasks] = useState(initialTasks);
useReducer
ile şu şekilde değiştirebilirsiniz:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
useReducer
hook’u useState
’e benzer; ona bir başlangıç state’i iletmeniz gerekir ve o da state bilgisi olan bir değeri geri döndürür (bu durumda dispatch fonksiyonu). Fakat yine de biraz faklılıklar gösterir.
useReducer
hook’u iki argüman alır:
- Bir reduce fonksiyonu
- Bir başlangıç state’i
Ve şunları geri döndürür:
- State bilgisi içeren bir değer
- Bir dispatch fonksiyonu (kullanıcı işlemleri reducer’a “göndermek” için)
Artık tüm bağlantılar kurulmuş halde! Reducer burada bileşen dosyasının en altında tanımlanmıştır:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId, }); } return ( <> <h1>Prag Gezisi Planı</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } case 'changed': { return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter((t) => t.id !== action.id); } default: { throw Error('Bilinmeyen işlem: ' + action.type); } } } let nextId = 3; const initialTasks = [ {id: 0, text: 'Kafka Müzesini ziyaret et', done: true}, {id: 1, text: 'Kukla gösterisi izle', done: false}, {id: 2, text: "Lennon Duvarı'nda fotoğraf çek", done: false}, ];
Hatta isterseniz reducer’ı ayrı bir dosyaya da taşıyabilirsiniz:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import tasksReducer from './tasksReducer.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId, }); } return ( <> <h1>Prag Gezisi Planı</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Kafka Müzesini ziyaret et', done: true}, {id: 1, text: 'Kukla gösterisi izle', done: false}, {id: 2, text: "Lennon Duvarı'nda fotoğraf çek", done: false}, ];
Bileşen mantığını bu şekilde ayırmak okunarlığı daha kolay hale getirebilir. Artık olay yöneticileri işlemleri göndererek yalnızca ne olduğunu belirler ve reducer fonksiyonu bunlara yanıt olarak state’in nasıl güncelleneceği kararını verir.
useState
ve useReducer
karışılaştırması
Reducers’ların dezavantajları da yok değil! İşte bunları karşılaştırabileceğiniz birkaç yol:
- Kod miktarı: Genellikle
useState
ile ilk etapta daha az kod yazmanız gerekir.useReducer
ile ise hem reducer fonksiyonu hem de dispatch işlemlerini yazmanız gerekir. Ne var ki, birçok olay yöneticisi state’i benzer şekillerde değiştiriyorsa,useReducer
kodu azaltmaya yardımcı olabilir. - Okunabilirlik:
useState
state güncellemeleri basit olduğu zamanlarda okunması da kolaydır. Daha karmaşık hale geldiklerinde ise bileşeninizin kodunu şişirebilir ve taranmasını zorlaştırabilir. Bu gibi durumlardauseReducer
, güncelleme mantığının nasıl olduğunu, olay yöneticilerinin ne olduğundan temiz bir şekilde ayırmanızı sağlar. - Hata ayıklama:
useState
ile ilgili bir hatanız olduğunda, state’in nerede ve neden yanlış ayarlandığını söylemek zor olabilir.useReducer
ile, her state güncellemesini ve bunun neden olduğunu (hangiişlem
’den kaynaklandığını) görmek için reducer’ınıza bir konsol logu ekleyebilirsiniz. Eğer bütün işlemler doğruysa, hatanın reducer mantığının kendisinde olduğunu bilirsiniz. Ancak,useState
ile olduğundan daha fazla kod üzerinden geçmeniz gerekir. - Test etme: Reducer, bileşeninize bağlı olmayan saf bir fonksiyondur. Bu, onu izole olarak ayrı ayrı dışa aktarabileceğiniz ve test edebileceğiniz anlamına gelir. Genellikle bileşenleri daha gerçekçi bir ortamda test etmek en iyisi olsa da karmaşık state güncelleme mantığı için reducer’unuzun belirli bir başlangıç state’i ve işlem için belirli bir state döndürdüğünü doğrulamak yararlı olabilir.
- Kişisel tercih: Bazı insanlar reducer’ları sever, bazıları sevmez. Bu sorun değil. Bu bir tercih meselesidir. Her zaman
useState
veuseReducer
arasında dönüşümlü olarak geçiş yapabilirsiniz: bunlar eşdeğerdir!
Bazı bileşenlerde yanlış state güncellemeleri nedeniyle sık sık hatalarla karşılaşıyorsanız ve koda daha fazla yapılandırma getirmek istiyorsanız bir reducer kullanmanızı öneririz. Her şey için reducer kullanmak zorunda değilsiniz: farklı kombinler yapmaktan çekinmeyin! Hatta aynı bileşende useState
ve useReducer
bile kullanabilirsiniz.
İyi bir reducer yazmak
Reducer yazarken şu iki ipucunu aklınızda bulundurun:
- Reducer’lar saf olmalıdır. State güncelleme fonksiyonları gibi, reducer’lar da render sırasında çalışır! (İşlemler bir sonraki render işlemine kadar sıraya alınır.) Bunun anlamı reducer’ların saf olması gerektiğidir; aynı girdiler her zaman aynı çıktıyla sonuçlanır. Bunlar istek göndermemeli, zaman aşımı planlamamalı veya herhangi bir yan etki (bileşenin dışındaki şeyleri etkileyen faaliyetler) gerçekleştirmemelidir. Nesneleri ve dizileri mutasyon (değişinim) olmadan güncellemelidirler.
- Her işlem verilerde birden fazla değişikliğe yol açsa bile tek bir kullanıcı etkileşimini ifade eder. Örneğin, bir reducer tarafından yönetilen beş alana sahip bir formda bir kullanıcı “Sıfırla” düğmesine bastığında, beş ayrı
set_field
işlemi yerine tek birreset_form
işlemini göndermek daha mantıklıdır. Bir reducer’daki her işlemi loglarsanız, bu log hangi etkileşimlerin veya yanıtların hangi sırayla gerçekleştiğini yeniden yapılandırmanız için yeterince açık olmalıdır. Bu, hata ayıklamaya yardımcı olur!
Immer ile kısa reducer’lar yazma
Aynı normal state’deki nesneleri ve dizileri güncelleme gibi, Immer kütüphanesini reducer’ları daha kısa ve öz hale getirmek için kullanabilirsiniz. Burada, useImmerReducer
işlevi push
veya arr[i] =
ataması ile state’i değiştirmenizi sağlar:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Reducer’lar saf olmalıdır, dolayısıyla state’i değiştirmemelidirler. Ancak Immer size mutasyona uğraması güvenli olan özel bir draft
nesnesi sağlar. Arka planda, Immer draft
’ta yaptığınız değişikliklerle state’inizin bir kopyasını oluşturacaktır. Bu nedenle, useImmerReducer
tarafından yönetilen reducer’lar ilk argümanlarını değiştirebilir ve state geri döndürmeleri gerekmez.
Özet
useState
’tenuseReducer
’a geçmek için:- İşlemlerinizi olay yöneticilerinden gönderin.
- Belirli bir state ve action için bir sonraki state’i döndüren bir reducer fonksiyonu yazın.
useState
’iuseReducer
ile değiştirin.
- Reducer’lar biraz daha fazla kod yazmanızı gerektirse de hata ayıklama ve test etme konusunda yardımcı olurlar.
- Reducer’lar saf olmalıdır.
- Her işlem tek bir kullanıcı etkileşimini ifade eder.
- Reducer’ları mutasyona uğrayan bir biçimde yazmak istiyorsanız Immer kullanın.
Problem 1 / 4: İşlemleri olay yöneticisinden gönderin
Şu anda, ContactList.js
ve Chat.js
içindeki olay yöneticilerinde // TODO
yorumları var. Bu nedenle yazı alanına yazma özelliği çalışmıyor ve düğmelere tıklamak seçilen alıcı kişiyi değiştirmiyor.
Bu iki // TODO
’yu ilgili işlemleri (actions) dispatch
edecek kodla değiştirin. İşlemlerin beklenen şeklini ve türünü görmek için messengerReducer.js
dosyasındaki reducer’u kontrol edin. Reducer hali hazırda yazılmıştır, bu yüzden değiştirmenize gerek yoktur. Yalnızca ContactList.js
ve Chat.js
içindeki işlemleri göndermeniz (dispatch) gerekir.
import { useReducer } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; import { initialState, messengerReducer } from './messengerReducer'; export default function Messenger() { const [state, dispatch] = useReducer(messengerReducer, initialState); const message = state.message; const contact = contacts.find((c) => c.id === state.selectedId); return ( <div> <ContactList contacts={contacts} selectedId={state.selectedId} dispatch={dispatch} /> <Chat key={contact.id} message={message} contact={contact} dispatch={dispatch} /> </div> ); } const contacts = [ {id: 0, name: 'Deniz', email: 'deniz@mail.com'}, {id: 1, name: 'Aylin', email: 'aylin@mail.com'}, {id: 2, name: 'Ata', email: 'ata@mail.com'}, ];