Reducer ve Context ile Ölçeklendirme
Reducer’lar bir bileşenin state güncelleme mantığını bir araya getirmenizi sağlar. Context, bilgileri diğer bileşenlere derinlemesine iletmeye olanak tanır. Reducer’ları ve context’i bir araya getirerek karmaşık bir ekranın state’ini yönetebilirsiniz.
Bunları öğreneceksiniz
- Bir reducer context ile nasıl birleştirilir
- State ve dispatch’i props üzerinden iletmekten nasıl kaçınılır
- Context ve State mantığını ayrı bir dosyada nasıl tutabiliriz
Reducer’ı context ile birleştirmek
Reducerlara giriş bölümünden bu örnekte, state bir reducer tarafından yönetilmektedir. Reducer fonksiyonu tüm state güncelleme mantığını içerir ve bu dosyanın en alt kısmında belirtilir:
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>İstanbul'da bir gün</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 eylem: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Tarihi Yarımada Yürüyüşü.', done: true }, { id: 1, text: 'Galata Kulesi Ziyareti.', done: false }, { id: 2, text: 'Türk kahvesi iç.', done: false } ];
Reducer, olay yöneticilerini kısa ve öz tutmaya yardımcı olur. Ancak, uygulamanız büyüdükçe başka bir zorlukla karşılaşabilirsiniz. Şu anda, tasks
state’i ve dispatch
fonksiyonu yalnızca üst düzey TaskApp
bileşeninde mevcuttur. Diğer bileşenlerin görev listesini okumasına veya değiştirmesine izin vermek için, mevcut state’i ve onu değiştiren olay yöneticilerini açıkça prop olarak aktarmanız gerekir.
Örneğin, TaskApp
görevlerin listesini ve olay yöneticilerini TaskList
’e aktarır:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
Ayrıca TaskList
olay yöneticilerini Task
’e aktarır:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
Bunun gibi küçük bir örnekte bu yapı iyi çalışır, ancak ortada onlarca veya yüzlerce bileşen varsa, tüm state ve fonksiyonları aktarmak oldukça sinir bozucu olabilir!
Bu nedenle, bunları proplar aracılığıyla aktarmaya alternatif olarak, hem tasks
state’ini hem de dispatch
fonksiyonunu context’e yerleştirmek isteyebilirsiniz. Bu şekilde, hiyerarşide TaskApp
altındaki herhangi bir bileşen görevleri okuyabilir ve tekrarlanan “prop drilling” olmadan eylemleri gönderebilir.
Burada bir reducer’ı context ile nasıl birleştirebileceğiniz anlatılmıştır:
- Context’i Oluştur.
- Context’in içine state ve dispatch’i Yerleştir.
- Hiyerarşi’nin herhangi bir yerinde context’i Kullan.
Adım 1: Context’i oluşturun.
useReducer
hook’u mevcut tasks
ve bunları güncellemenizi sağlayan dispatch
fonksiyonunu döndürür:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Bunları hiyerarşide aşağı aktarmak için iki ayrı context oluşturacaksınız:
TasksContext
geçerli görev listesini sağlar.TasksDispatchContext
bileşenlerin eylemleri göndermesini sağlayan fonksiyonu sağlar.
Bunları ayrı bir dosyadan dışa aktarın, böylece daha sonra diğer dosyalardan içe aktarabilirsiniz:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Burada, her iki context de varsayılan değer olarak null
değerini veriyorsunuz. Gerçek değerler TaskApp
bileşeni tarafından sağlanacaktır.
Adım 2: Context’in içine state ve dispatch’i yerleştir
Artık her iki context’i de TaskApp
bileşeninize aktarabilirsiniz. useReducer()
tarafından döndürülen tasks
ve dispatch
bileşenlerini alın ve aşağıdaki hiyerarşinin tamamına sağlayın:
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Şimdilik, bilgileri hem prop’lar aracılığıyla hem de context içinde iletebilirsiniz:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksContext, TasksDispatchContext } from './TasksContext.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 ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> <h1>İstanbul'da bir gün</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext.Provider> </TasksContext.Provider> ); } 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 eylem: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Tarihi Yarımada Yürüyüşü.', done: true }, { id: 1, text: 'Galata Kulesi Ziyareti.', done: false }, { id: 2, text: 'Türk kahvesi iç.', done: false } ];
Bir sonraki adımda, prop geçişini kaldıracaksınız.
Adım 3: Hiyerarşi’nin herhangi bir yerinde context’i kullan
Artık görevlerin listesini veya olay yöneticilerini hiyerarşi boyunca iletmek zorunda değilsiniz:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>İstanbul'da bir gün</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
Bunun yerine, görev listesine ihtiyaç duyan herhangi bir bileşen bunu TaskContext
’ten okuyabilir:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
Görev listesini güncellemek için, herhangi bir bileşen dispatch
fonksiyonunu context’den okuyabilir ve çağırabilir:
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Ekle</button>
// ...
TaskApp
bileşeni herhangi bir olay yöneticisini aşağıya iletmemekte ve TaskList
bileşeni de Task
bileşenine herhangi bir olay yöneticisini iletmemektedir. Her bileşen ihtiyacı olan context’i okur:
import { useState, useContext } from 'react'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useContext(TasksDispatchContext); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Kaydet </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Düzenle </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Sil </button> </label> ); }
State hala useReducer
ile yönetilen en üst düzey TaskApp
bileşeninde “barınıyor”. Ancak tasks
ve dispatch
artık bu contextleri içe aktarıp kullanarak hiyerarşi’de aşağıdaki her bileşen tarafından kullanılabilir.
Tüm bağlantıları tek bir dosyaya taşıma
Bunu yapmak zorunda değilsiniz, ancak hem reducer hem de context tek bir dosyaya taşıyarak bileşenleri daha da sadeleştirebilirsiniz. Şu anda, TasksContext.js
sadece iki context bildirimi içermektedir:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
Bu dosya kalabalıklaşmak üzere! Reducer’ı aynı dosyaya taşıyacaksınız. Ardından aynı dosyada yeni bir TasksProvider
bileşeni tanımlayacaksınız. Bu bileşen tüm parçaları birbirine bağlayacak:
- Reducer’la state’i yönetecek.
- Aşağıdaki bileşenlere her iki context’i de sağlayacaktır.
children
’ı bir prop olarak alacak, böylece ona JSX’i iletebilirsiniz.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
Bu, TaskApp
bileşeninizden tüm karmaşıklığı ve bağlantıyı kaldırır:
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>İstanbul'da bir gün</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
Ayrıca context’i kullanan fonksiyonları TasksContext.js
dosyasından dışa aktarabilirsiniz:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Bir bileşenin context’i okuması gerektiğinde, bunu bu fonksiyonlar aracılığıyla yapabilir:
const tasks = useTasks();
const dispatch = useTasksDispatch();
Bu, davranışı herhangi bir şekilde değiştirmez, ancak daha sonra bu contextleri daha da bölmenize veya bu fonksiyonlara bazı mantıklar eklemenize olanak tanır. Artık tüm context ve reducer bağlantıları TasksContext.js
içindedir. Bu, bileşenleri temiz ve düzensiz olmayan, verileri nereden aldıklarından ziyade neyi görüntülediklerine odaklanmış tutar.
import { useState } from 'react'; import { useTasks, useTasksDispatch } from './TasksContext.js'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Kaydet </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Düzenle </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Sil </button> </label> ); }
TasksProvider
’ı görevleri nasıl işleyeceğini bilen bir ekran parçası olarak düşünebilirsiniz, useTasks
onları okumanın bir yolu olarak, ve useTasksDispatch
onları hiyerarşideki herhangi bir bileşenden güncellemenin bir yolu olarak düşünülebilir.
Uygulamanız büyüdükçe, bunun gibi birçok context-reducer çiftine sahip olabilirsiniz. Bu, uygulamanızı ölçeklendirmenin ve hiyerarşinin derinliklerindeki verilere erişmek istediğinizde çok fazla iş yapmadan state’i yükseltmenin güçlü bir yoludur.
Özet
- Herhangi bir bileşenin üzerindeki state’i okumasına ve güncellemesine izin vermek için reducer’ı context ile birleştirebilirsiniz.
- Aşağıdakiler bileşenlere state ve dispatch fonksiyonu sağlamak için:
- İki context oluşturun (state ve dispatch fonksiyonları için).
- Reducer’ı kullanan bileşende her iki context’i de sağlayın.
- Bunları okuması gereken bileşenlerin contextlerinden birini kullanın.
- Bileşenleri daha da temizleyebilirsiniz; tüm bağlantıları tek bir dosyaya taşıyarak.
- Context sağlayan
TasksProvider
gibi bir bileşeni dışa aktarabilirsiniz. - Ayrıca okumak için
useTasks
veuseTasksDispatch
gibi özel hook’ları da dışa aktarabilirsiniz.
- Context sağlayan
- Uygulamanızda bunun gibi birçok context-reducer çiftine sahip olabilirsiniz.