Bileşik bileşenler, dinamik context'ler ve karmaşık durum yönetimi için optimize edilmiş performans teknikleri dahil olmak üzere React Context API için gelişmiş kalıpları keşfedin.
Durum Yönetimi için Gelişmiş React Context API Kalıpları
React Context API, prop drilling (prop aktarımı) yapmadan uygulamanız genelinde state paylaşımı için güçlü bir mekanizma sunar. Temel kullanımı basit olsa da, tam potansiyelinden yararlanmak, karmaşık durum yönetimi senaryolarını ele alabilen gelişmiş kalıpları anlamayı gerektirir. Bu makale, React geliştirmenizi bir üst seviyeye taşımak için pratik örnekler ve uygulanabilir bilgiler sunarak bu kalıplardan birkaçını incelemektedir.
Temel Context API'nin Sınırlılıklarını Anlamak
Gelişmiş kalıplara dalmadan önce, temel Context API'nin sınırlılıklarını kabul etmek çok önemlidir. Basit, global olarak erişilebilir state için uygun olsa da, sık değişen state'e sahip karmaşık uygulamalar için hantal ve verimsiz hale gelebilir. Bir context'i tüketen her bileşen, state'in güncellenen belirli kısmına bağlı olmasa bile, context değeri her değiştiğinde yeniden render olur. Bu durum performans darboğazlarına yol açabilir.
Kalıp 1: Context ile Bileşik Bileşenler (Compound Components)
Bileşik Bileşen kalıbı, bir context aracılığıyla dolaylı olarak state ve mantığı paylaşan bir dizi ilgili bileşen oluşturarak Context API'yi geliştirir. Bu kalıp, yeniden kullanılabilirliği teşvik eder ve tüketiciler için API'yi basitleştirir. Bu, karmaşık mantığın basit bir uygulama ile kapsüllenmesine olanak tanır.
Örnek: Bir Sekme (Tab) Bileşeni
Bunu bir Sekme bileşeniyle gösterelim. Prop'ları birden çok katman üzerinden geçirmek yerine, Tab
bileşenleri paylaşılan bir context aracılığıyla dolaylı olarak iletişim kurar.
// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface TabContextType {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabContext = createContext(undefined);
interface TabProviderProps {
children: ReactNode;
defaultTab: string;
}
export const TabProvider: React.FC = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
const value: TabContextType = {
activeTab,
setActiveTab,
};
return {children} ;
};
export const useTabContext = () => {
const context = useContext(TabContext);
if (!context) {
throw new Error('useTabContext must be used within a TabProvider');
}
return context;
};
// TabList.js
import React, { ReactNode } from 'react';
interface TabListProps {
children: ReactNode;
}
export const TabList: React.FC = ({ children }) => {
return {children};
};
// Tab.js
import React, { ReactNode } from 'react';
import { useTabContext } from './TabContext';
interface TabProps {
label: string;
children: ReactNode;
}
export const Tab: React.FC = ({ label, children }) => {
const { activeTab, setActiveTab } = useTabContext();
const isActive = activeTab === label;
const handleClick = () => {
setActiveTab(label);
};
return (
);
};
// TabPanel.js
import React, { ReactNode } from 'react';
import { useTabContext } from './TabContext';
interface TabPanelProps {
label: string;
children: ReactNode;
}
export const TabPanel: React.FC = ({ label, children }) => {
const { activeTab } = useTabContext();
const isActive = activeTab === label;
return (
{isActive && children}
);
};
// Kullanım
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';
function App() {
return (
Tab 1
Tab 2
Tab 3
Content for Tab 1
Content for Tab 2
Content for Tab 3
);
}
export default App;
Faydaları:
- Basitleştirilmiş API for tüketiciler için: Kullanıcıların yalnızca
Tab
,TabList
veTabPanel
hakkında endişelenmesi gerekir. - Örtük state paylaşımı: Bileşenler, paylaşılan state'e otomatik olarak erişir ve onu günceller.
- Geliştirilmiş yeniden kullanılabilirlik:
Tab
bileşeni farklı bağlamlarda kolayca yeniden kullanılabilir.
Kalıp 2: Dinamik Context'ler
Bazı senaryolarda, bileşenin bileşen ağacındaki konumuna veya diğer dinamik faktörlere bağlı olarak farklı context değerlerine ihtiyaç duyabilirsiniz. Dinamik context'ler, belirli koşullara göre değişen context değerleri oluşturmanıza ve sağlamanıza olanak tanır.
Örnek: Dinamik Context'lerle Tema Yönetimi
Kullanıcının tercihlerine veya uygulamanın bulundukları bölümüne göre farklı temalar sunmak istediğiniz bir tema sistemi düşünün. Açık ve koyu tema ile basitleştirilmiş bir örnek yapabiliriz.
// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
background: 'white',
color: 'black'
};
const darkTheme: Theme = {
background: 'black',
color: 'white'
};
const ThemeContext = createContext({
theme: defaultTheme,
toggleTheme: () => {}
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const theme = isDarkTheme ? darkTheme : defaultTheme;
const toggleTheme = () => {
setIsDarkTheme(!isDarkTheme);
};
const value: ThemeContextType = {
theme,
toggleTheme,
};
return {children} ;
};
export const useTheme = () => {
return useContext(ThemeContext);
};
// Kullanım
import { useTheme, ThemeProvider } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
Bu temalı bir bileşendir.
);
}
function App() {
return (
);
}
export default App;
Bu örnekte, ThemeProvider
, isDarkTheme
state'ine göre temayı dinamik olarak belirler. useTheme
hook'unu kullanan bileşenler, tema değiştiğinde otomatik olarak yeniden render olacaktır.
Kalıp 3: Karmaşık State için Context ve useReducer
Karmaşık state mantığını yönetmek için, Context API'yi useReducer
ile birleştirmek mükemmel bir yaklaşımdır. useReducer
, state'i eylemlere (action) göre güncellemek için yapılandırılmış bir yol sağlar ve Context API, bu state'i ve dispatch fonksiyonunu uygulamanız genelinde paylaşmanıza olanak tanır.
Örnek: Basit Bir Yapılacaklar Listesi
// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
interface TodoContextType {
state: TodoState;
dispatch: React.Dispatch;
}
const initialState: TodoState = {
todos: [],
};
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
};
const TodoContext = createContext(undefined);
interface TodoProviderProps {
children: ReactNode;
}
export const TodoProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(todoReducer, initialState);
const value: TodoContextType = {
state,
dispatch,
};
return {children} ;
};
export const useTodo = () => {
const context = useContext(TodoContext);
if (!context) {
throw new Error('useTodo must be used within a TodoProvider');
}
return context;
};
// Kullanım
import { useTodo, TodoProvider } from './TodoContext';
function TodoList() {
const { state, dispatch } = useTodo();
return (
{state.todos.map((todo) => (
-
{todo.text}
))}
);
}
function AddTodo() {
const { dispatch } = useTodo();
const [text, setText] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text });
setText('');
};
return (
);
}
function App() {
return (
);
}
export default App;
Bu kalıp, state yönetimi mantığını reducer içinde merkezileştirerek, anlaşılmasını ve test edilmesini kolaylaştırır. Bileşenler, state'i doğrudan yönetmek zorunda kalmadan state'i güncellemek için eylemler gönderebilir.
Kalıp 4: `useMemo` ve `useCallback` ile Optimize Edilmiş Context Güncellemeleri
Daha önce de belirtildiği gibi, Context API ile ilgili önemli bir performans hususu gereksiz yeniden render'lardır. useMemo
ve useCallback
kullanmak, yalnızca context değerinin gerekli kısımlarının güncellenmesini ve fonksiyon referanslarının kararlı kalmasını sağlayarak bu yeniden render'ları önleyebilir.
Örnek: Bir Tema Context'ini Optimize Etme
// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const defaultTheme: Theme = {
background: 'white',
color: 'black'
};
const darkTheme: Theme = {
background: 'black',
color: 'white'
};
const ThemeContext = createContext({
theme: defaultTheme,
toggleTheme: () => {}
});
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const theme = isDarkTheme ? darkTheme : defaultTheme;
const toggleTheme = useCallback(() => {
setIsDarkTheme(!isDarkTheme);
}, [isDarkTheme]);
const value: ThemeContextType = useMemo(() => ({
theme,
toggleTheme,
}), [theme, toggleTheme]);
return {children} ;
};
export const useTheme = () => {
return useContext(ThemeContext);
};
Açıklama:
useCallback
,toggleTheme
fonksiyonunu memoize eder (hafızaya alır). Bu, fonksiyon referansının yalnızcaisDarkTheme
değiştiğinde değişmesini sağlar ve yalnızcatoggleTheme
fonksiyonuna bağlı olan bileşenlerin gereksiz yere yeniden render olmasını önler.useMemo
, context değerini memoize eder. Bu, context değerinin yalnızcatheme
veyatoggleTheme
fonksiyonu değiştiğinde değişmesini sağlar ve gereksiz yeniden render'ları daha da önler.
useCallback
olmadan, toggleTheme
fonksiyonu ThemeProvider
'ın her render'ında yeniden oluşturulur, bu da value
'nun değişmesine ve tema kendisi değişmemiş olsa bile tüketen bileşenlerde yeniden render'ları tetiklemesine neden olur. useMemo
, yeni bir value
'nun yalnızca bağımlılıkları (theme
veya toggleTheme
) değiştiğinde oluşturulmasını sağlar.
Kalıp 5: Context Seçicileri (Selectors)
Context seçicileri, bileşenlerin yalnızca context değerinin belirli kısımlarına abone olmalarını sağlar. Bu, context'in diğer kısımları değiştiğinde gereksiz yeniden render'ları önler. Bunu başarmak için `use-context-selector` gibi kütüphaneler veya özel implementasyonlar kullanılabilir.
Örnek: Özel Bir Context Seçici Kullanımı
// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';
function useCustomContextSelector(
context: React.Context,
selector: (value: T) => S
): S {
const value = useContext(context);
const [selected, setSelected] = useState(() => selector(value));
const latestSelector = useRef(selector);
latestSelector.current = selector;
useEffect(() => {
let didUnmount = false;
let lastSelected = selected;
const subscription = () => {
if (didUnmount) {
return;
}
const nextSelected = latestSelector.current(value);
if (!Object.is(lastSelected, nextSelected)) {
lastSelected = nextSelected;
setSelected(nextSelected);
}
};
// Normalde burada context değişikliklerine abone olursunuz. Bu basitleştirilmiş bir
// örnek olduğundan, başlatmak için subscription'ı hemen çağıracağız.
subscription();
return () => {
didUnmount = true;
// Varsa, burada context değişikliklerinden aboneliği kaldırın.
};
}, [value]); // Context değeri her değiştiğinde effect'i yeniden çalıştır
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (Kısalık için basitleştirilmiştir)
import React, { createContext, useState, ReactNode } from 'react';
interface Theme {
background: string;
color: string;
}
interface ThemeContextType {
theme: Theme;
setTheme: (newTheme: Theme) => void;
}
const ThemeContext = createContext(undefined);
interface ThemeProviderProps {
children: ReactNode;
initialTheme: Theme;
}
export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
const [theme, setTheme] = useState(initialTheme);
const value: ThemeContextType = {
theme,
setTheme
};
return {children} ;
};
export const useThemeContext = () => {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error("useThemeContext must be used within a ThemeProvider");
}
return context;
};
export default ThemeContext;
// Kullanım
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';
function BackgroundComponent() {
const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
return Background;
}
function ColorComponent() {
const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color);
return Color;
}
function App() {
const { theme, setTheme } = useThemeContext();
const toggleTheme = () => {
setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' });
};
return (
);
}
export default App;
Bu örnekte, BackgroundComponent
yalnızca temanın background
özelliği değiştiğinde yeniden render olur ve ColorComponent
yalnızca color
özelliği değiştiğinde yeniden render olur. Bu, tüm context değeri değiştiğinde gereksiz yeniden render'ları önler.
Kalıp 6: Eylemleri (Actions) State'ten Ayırma
Daha büyük uygulamalar için, context değerini iki ayrı context'e ayırmayı düşünün: biri state için ve diğeri eylemler (dispatch fonksiyonları) için. Bu, kod organizasyonunu ve test edilebilirliği artırabilir.
Örnek: Ayrı State ve Eylem Context'leri ile Yapılacaklar Listesi
// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
const initialState: TodoState = {
todos: [],
};
const TodoStateContext = createContext(initialState);
interface TodoStateProviderProps {
children: ReactNode;
}
export const TodoStateProvider: React.FC = ({ children }) => {
const [state] = useReducer(todoReducer, initialState);
return {children} ;
};
export const useTodoState = () => {
return useContext(TodoStateContext);
};
// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
const TodoActionContext = createContext | undefined>(undefined);
interface TodoActionProviderProps {
children: ReactNode;
}
export const TodoActionProvider: React.FC = ({children}) => {
const [, dispatch] = useReducer(todoReducer, initialState);
return {children} ;
};
export const useTodoDispatch = () => {
const dispatch = useContext(TodoActionContext);
if (!dispatch) {
throw new Error('useTodoDispatch must be used within a TodoActionProvider');
}
return dispatch;
};
// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.id),
};
default:
return state;
}
};
// Kullanım
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';
function TodoList() {
const state = useTodoState();
return (
{state.todos.map((todo) => (
-
{todo.text}
))}
);
}
function TodoActions({ todo }) {
const dispatch = useTodoDispatch();
return (
<>
>
);
}
function AddTodo() {
const dispatch = useTodoDispatch();
const [text, setText] = React.useState('');
const handleSubmit = (e) => {
e.preventDefault();
dispatch({ type: 'ADD_TODO', text });
setText('');
};
return (
);
}
function App() {
return (
);
}
export default App;
Bu ayırma, bileşenlerin yalnızca ihtiyaç duydukları context'e abone olmalarını sağlayarak gereksiz yeniden render'ları azaltır. Ayrıca reducer'ı ve her bir bileşeni izole bir şekilde birim testine tabi tutmayı kolaylaştırır. Ayrıca, provider sarmalama sırası önemlidir. ActionProvider
'ın StateProvider
'ı sarmalaması gerekir.
En İyi Uygulamalar ve Dikkat Edilmesi Gerekenler
- Context, tüm state yönetimi kütüphanelerinin yerini almamalıdır: Çok büyük ve karmaşık uygulamalar için Redux veya Zustand gibi özel state yönetimi kütüphaneleri hala daha iyi bir seçenek olabilir.
- Aşırı context kullanımından kaçının: Her state parçasının bir context içinde olması gerekmez. Context'i gerçekten global veya yaygın olarak paylaşılan state için akıllıca kullanın.
- Performans testi: Özellikle sık güncellenen state ile uğraşırken, context kullanımınızın performans etkisini her zaman ölçün.
- Kod Bölme (Code Splitting): Context API kullanırken, uygulamanızı daha küçük parçalara ayırmayı düşünün. Bu, özellikle state'teki küçük bir değişikliğin uygulamanın büyük bir bölümünün yeniden render olmasına neden olduğu durumlarda önemlidir.
Sonuç
React Context API, state yönetimi için çok yönlü bir araçtır. Bu gelişmiş kalıpları anlayarak ve uygulayarak, karmaşık state'i etkili bir şekilde yönetebilir, performansı optimize edebilir ve daha sürdürülebilir ve ölçeklenebilir React uygulamaları oluşturabilirsiniz. Özel ihtiyaçlarınız için doğru kalıbı seçmeyi ve context kullanımınızın performans etkilerini dikkatlice değerlendirmeyi unutmayın.
React geliştikçe, Context API'yi çevreleyen en iyi uygulamalar da gelişecektir. Yeni teknikler ve kütüphaneler hakkında bilgi sahibi olmak, modern web geliştirmenin state yönetimi zorluklarının üstesinden gelmek için donanımlı olmanızı sağlayacaktır. Daha da hassas tepkisellik için sinyallerle (signals) context kullanmak gibi ortaya çıkan yeni kalıpları keşfetmeyi düşünün.