Prozkoumejte pokročilé vzory pro React Context API, včetně složených komponent, dynamických kontextů a optimalizačních technik pro správu složitého stavu.
Pokročilé vzory React Context API pro správu stavu
React Context API poskytuje mocný mechanismus pro sdílení stavu napříč vaší aplikací bez tzv. prop drillingu. Zatímco základní použití je jednoduché, využití jeho plného potenciálu vyžaduje pochopení pokročilých vzorů, které si poradí se složitými scénáři správy stavu. Tento článek prozkoumává několik těchto vzorů a nabízí praktické příklady a užitečné poznatky pro pozvednutí vašeho vývoje v Reactu.
Pochopení omezení základního Context API
Než se ponoříme do pokročilých vzorů, je klíčové si uvědomit omezení základního Context API. I když je vhodné pro jednoduchý, globálně dostupný stav, může se stát těžkopádným a neefektivním pro komplexní aplikace s často se měnícím stavem. Každá komponenta, která konzumuje kontext, se překreslí pokaždé, když se hodnota kontextu změní, i když komponenta nezávisí na konkrétní části stavu, která byla aktualizována. To může vést k problémům s výkonem.
Vzor 1: Složené komponenty (Compound Components) s kontextem
Vzor složených komponent vylepšuje Context API vytvořením sady souvisejících komponent, které implicitně sdílejí stav a logiku prostřednictvím kontextu. Tento vzor podporuje znovupoužitelnost a zjednodušuje API pro konzumenty. To umožňuje zapouzdřit složitou logiku s jednoduchou implementací.
Příklad: Komponenta pro záložky (Tab)
Ukažme si to na příkladu komponenty pro záložky (Tab). Místo předávání props přes několik vrstev komponenty Tab
implicitně komunikují prostřednictvím sdíleného kontextu.
// 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}
);
};
// Použití
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';
function App() {
return (
Záložka 1
Záložka 2
Záložka 3
Obsah pro Záložku 1
Obsah pro Záložku 2
Obsah pro Záložku 3
);
}
export default App;
Výhody:
- Zjednodušené API pro konzumenty: Uživatelé se musí starat pouze o
Tab
,TabList
aTabPanel
. - Implicitní sdílení stavu: Komponenty automaticky přistupují ke sdílenému stavu a aktualizují ho.
- Zlepšená znovupoužitelnost: Komponentu
Tab
lze snadno znovu použít v různých kontextech.
Vzor 2: Dynamické kontexty
V některých scénářích můžete potřebovat různé hodnoty kontextu na základě pozice komponenty ve stromu komponent nebo jiných dynamických faktorů. Dynamické kontexty vám umožňují vytvářet a poskytovat hodnoty kontextu, které se mění na základě specifických podmínek.
Příklad: Témata s dynamickými kontexty
Představte si systém témat, kde chcete poskytovat různá témata na základě preferencí uživatele nebo sekce aplikace, ve které se nachází. Můžeme si vytvořit zjednodušený příklad se světlým a tmavým tématem.
// 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);
};
// Použití
import { useTheme, ThemeProvider } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
Toto je komponenta s tématem.
);
}
function App() {
return (
);
}
export default App;
V tomto příkladu ThemeProvider
dynamicky určuje téma na základě stavu isDarkTheme
. Komponenty používající hook useTheme
se automaticky překreslí, když se téma změní.
Vzor 3: Kontext s useReducer pro složitý stav
Pro správu složité logiky stavu je kombinace Context API s useReducer
vynikajícím přístupem. useReducer
poskytuje strukturovaný způsob aktualizace stavu na základě akcí a Context API umožňuje sdílet tento stav a funkci dispatch napříč vaší aplikací.
Příklad: Jednoduchý seznam úkolů (Todo List)
// 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;
};
// Použití
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;
Tento vzor centralizuje logiku správy stavu v reduceru, což usnadňuje přemýšlení o kódu a jeho testování. Komponenty mohou odesílat akce k aktualizaci stavu, aniž by musely stav spravovat přímo.
Vzor 4: Optimalizované aktualizace kontextu s useMemo
a useCallback
Jak bylo zmíněno dříve, klíčovým aspektem výkonu u Context API jsou zbytečná překreslování. Použití useMemo
a useCallback
může těmto překreslováním zabránit tím, že zajistí aktualizaci pouze nezbytných částí hodnoty kontextu a že reference na funkce zůstanou stabilní.
Příklad: Optimalizace kontextu pro téma
// 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);
};
Vysvětlení:
useCallback
memoizuje funkcitoggleTheme
. Tím je zajištěno, že se reference na funkci změní pouze tehdy, když se změníisDarkTheme
, což zabraňuje zbytečnému překreslování komponent, které závisí pouze na funkcitoggleTheme
.useMemo
memoizuje hodnotu kontextu. Tím je zajištěno, že se hodnota kontextu změní pouze tehdy, když se změní buďtheme
nebo funkcetoggleTheme
, což dále zabraňuje zbytečným překreslováním.
Bez useCallback
by se funkce toggleTheme
znovu vytvářela při každém renderu ThemeProvider
, což by způsobilo změnu value
a vyvolalo překreslení ve všech konzumujících komponentách, i kdyby se samotné téma nezměnilo. useMemo
zajišťuje, že nová value
je vytvořena pouze tehdy, když se změní její závislosti (theme
nebo toggleTheme
).
Vzor 5: Selektory kontextu (Context Selectors)
Selektory kontextu umožňují komponentám přihlásit se k odběru pouze specifických částí hodnoty kontextu. Tím se zabrání zbytečným překreslováním, když se změní jiné části kontextu. K dosažení tohoto cíle lze použít knihovny jako `use-context-selector` nebo vlastní implementace.
Příklad použití vlastního selektoru kontextu
// 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);
}
};
// Zde byste se obvykle přihlásili k odběru změn kontextu. Jelikož se jedná o zjednodušený
// příklad, zavoláme subscription okamžitě pro inicializaci.
subscription();
return () => {
didUnmount = true;
// Zde se odhlásíte z odběru změn kontextu, pokud je to relevantní.
};
}, [value]); // Spustí efekt znovu, kdykoli se změní hodnota kontextu
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (Zjednodušeno pro stručnost)
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;
// Použití
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';
function BackgroundComponent() {
const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
return Pozadí;
}
function ColorComponent() {
const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color);
return Barva;
}
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;
V tomto příkladu se BackgroundComponent
překreslí pouze tehdy, když se změní vlastnost background
tématu, a ColorComponent
se překreslí pouze tehdy, když se změní vlastnost color
. Tím se zabrání zbytečným překreslováním, když se změní celá hodnota kontextu.
Vzor 6: Oddělení akcí od stavu
U větších aplikací zvažte rozdělení hodnoty kontextu do dvou samostatných kontextů: jeden pro stav a druhý pro akce (funkce dispatch). To může zlepšit organizaci kódu a testovatelnost.
Příklad: Seznam úkolů s oddělenými kontexty pro stav a akce
// 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;
}
};
// Použití
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;
Toto oddělení umožňuje komponentám přihlásit se k odběru pouze toho kontextu, který potřebují, čímž se snižuje počet zbytečných překreslení. Také usnadňuje jednotkové testování reduceru a každé komponenty izolovaně. Dále záleží na pořadí vnoření providerů. ActionProvider
musí obalovat StateProvider
.
Osvědčené postupy a doporučení
- Kontext by neměl nahradit všechny knihovny pro správu stavu: Pro velmi rozsáhlé a komplexní aplikace mohou být specializované knihovny pro správu stavu jako Redux nebo Zustand stále lepší volbou.
- Vyhněte se nadměrnému používání kontextu: Ne každý kousek stavu musí být v kontextu. Používejte kontext uvážlivě pro skutečně globální nebo široce sdílený stav.
- Testování výkonu: Vždy měřte dopad použití kontextu na výkon, zejména pokud pracujete s často se aktualizujícím stavem.
- Rozdělování kódu (Code Splitting): Při používání Context API zvažte rozdělení vaší aplikace na menší části. To je zvláště důležité, když malá změna stavu způsobí překreslení velké části aplikace.
Závěr
React Context API je všestranný nástroj pro správu stavu. Pochopením a aplikací těchto pokročilých vzorů můžete efektivně spravovat složitý stav, optimalizovat výkon a vytvářet udržovatelnější a škálovatelnější React aplikace. Nezapomeňte si vybrat správný vzor pro vaše specifické potřeby a pečlivě zvážit dopady použití kontextu na výkon.
Jak se React vyvíjí, tak se budou vyvíjet i osvědčené postupy týkající se Context API. Zůstat informován o nových technikách a knihovnách vám zajistí, že budete připraveni čelit výzvám správy stavu v moderním webovém vývoji. Zvažte prozkoumání nově vznikajících vzorů, jako je použití kontextu se signály pro ještě jemněji granulovanou reaktivitu.