Istražite napredne obrasce za React Context API, uključujući složene komponente, dinamičke kontekste i tehnike optimizacije performansi za složeno upravljanje stanjem.
Napredni obrasci React Context API-ja za upravljanje stanjem
React Context API pruža moćan mehanizam za dijeljenje stanja kroz aplikaciju bez potrebe za "prop drillingom". Iako je osnovna upotreba jednostavna, iskorištavanje punog potencijala zahtijeva razumijevanje naprednih obrazaca koji mogu rješavati složene scenarije upravljanja stanjem. Ovaj članak istražuje nekoliko tih obrazaca, nudeći praktične primjere i korisne uvide za podizanje vašeg React razvoja na višu razinu.
Razumijevanje ograničenja osnovnog Context API-ja
Prije nego što zaronimo u napredne obrasce, ključno je prepoznati ograničenja osnovnog Context API-ja. Iako je prikladan za jednostavno, globalno dostupno stanje, može postati nespretan i neučinkovit za složene aplikacije s često mijenjajućim stanjem. Svaka komponenta koja koristi kontekst ponovno se renderira svaki put kad se vrijednost konteksta promijeni, čak i ako komponenta ne ovisi o specifičnom dijelu stanja koji je ažuriran. To može dovesti do uskih grla u performansama.
Obrazac 1: Složene komponente (Compound Components) s kontekstom
Obrazac složenih komponenata (Compound Component) poboljšava Context API stvaranjem skupa povezanih komponenata koje implicitno dijele stanje i logiku putem konteksta. Ovaj obrazac promiče ponovnu upotrebljivost i pojednostavljuje API za korisnike. To omogućuje da se složena logika enkapsulira jednostavnom implementacijom.
Primjer: Komponenta s karticama (Tab)
Ilustrirajmo to s komponentom za kartice (Tab). Umjesto prosljeđivanja propsa kroz više slojeva, Tab
komponente implicitno komuniciraju putem zajedničkog konteksta.
// 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}
);
};
// Upotreba
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;
Prednosti:
- Pojednostavljeni API za korisnike: Korisnici se trebaju brinuti samo o komponentama
Tab
,TabList
iTabPanel
. - Implicitno dijeljenje stanja: Komponente automatski pristupaju i ažuriraju zajedničko stanje.
- Poboljšana ponovna upotrebljivost: Komponenta
Tab
može se lako ponovno koristiti u različitim kontekstima.
Obrazac 2: Dinamički konteksti
U nekim scenarijima, možda će vam trebati različite vrijednosti konteksta ovisno o poziciji komponente u stablu komponenata ili drugim dinamičkim faktorima. Dinamički konteksti omogućuju vam stvaranje i pružanje vrijednosti konteksta koje variraju ovisno o specifičnim uvjetima.
Primjer: Teme s dinamičkim kontekstima
Razmotrimo sustav tema gdje želite pružiti različite teme ovisno o korisničkim postavkama ili dijelu aplikacije u kojem se nalaze. Možemo napraviti pojednostavljeni primjer sa svijetlom i tamnom temom.
// 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);
};
// Upotreba
import { useTheme, ThemeProvider } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
This is a themed component.
);
}
function App() {
return (
);
}
export default App;
U ovom primjeru, ThemeProvider
dinamički određuje temu na temelju stanja isDarkTheme
. Komponente koje koriste hook useTheme
automatski će se ponovno renderirati kada se tema promijeni.
Obrazac 3: Kontekst s useReducerom za složeno stanje
Za upravljanje složenom logikom stanja, kombiniranje Context API-ja s useReducer
-om je izvrstan pristup. useReducer
pruža strukturiran način ažuriranja stanja na temelju akcija, a Context API omogućuje dijeljenje tog stanja i dispatch funkcije kroz cijelu aplikaciju.
Primjer: Jednostavna lista zadataka (Todo)
// 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;
};
// Upotreba
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;
Ovaj obrazac centralizira logiku upravljanja stanjem unutar reducera, što olakšava razumijevanje i testiranje. Komponente mogu slati (dispatch) akcije za ažuriranje stanja bez potrebe da izravno upravljaju stanjem.
Obrazac 4: Optimizirana ažuriranja konteksta s `useMemo` i `useCallback`
Kao što je ranije spomenuto, ključna stavka performansi kod Context API-ja su nepotrebna ponovna renderiranja. Korištenje useMemo
i useCallback
može spriječiti ta ponovna renderiranja osiguravajući da se ažuriraju samo potrebni dijelovi vrijednosti konteksta i da reference funkcija ostanu stabilne.
Primjer: Optimizacija konteksta za teme
// 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);
};
Objašnjenje:
useCallback
memoizira funkcijutoggleTheme
. To osigurava da se referenca funkcije mijenja samo kada se promijeniisDarkTheme
, sprječavajući nepotrebna ponovna renderiranja komponenata koje ovise samo o funkcijitoggleTheme
.useMemo
memoizira vrijednost konteksta. To osigurava da se vrijednost konteksta mijenja samo kada se promijeni ilitheme
ili funkcijatoggleTheme
, dodatno sprječavajući nepotrebna ponovna renderiranja.
Bez useCallback
, funkcija toggleTheme
bila bi ponovno stvorena pri svakom renderiranju ThemeProvider
-a, što bi uzrokovalo promjenu value
i pokrenulo ponovna renderiranja u svim komponentama koje koriste kontekst, čak i ako se sama tema nije promijenila. useMemo
osigurava da se nova value
stvara samo kada se promijene njezine ovisnosti (theme
ili toggleTheme
).
Obrazac 5: Selektori konteksta
Selektori konteksta omogućuju komponentama da se pretplate samo na specifične dijelove vrijednosti konteksta. To sprječava nepotrebna ponovna renderiranja kada se drugi dijelovi konteksta promijene. Za postizanje ovoga mogu se koristiti biblioteke poput `use-context-selector` ili prilagođene implementacije.
Primjer korištenja prilagođenog selektora konteksta
// 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);
}
};
// Ovdje biste se obično pretplatili na promjene konteksta. Budući da je ovo pojednostavljeni
// primjer, samo ćemo odmah pozvati subscription za inicijalizaciju.
subscription();
return () => {
didUnmount = true;
// Ovdje se odjavite s promjena konteksta, ako je primjenjivo.
};
}, [value]); // Ponovno pokreni efekt svaki put kad se vrijednost konteksta promijeni
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (Pojednostavljeno radi sažetosti)
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;
// Upotreba
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;
U ovom primjeru, BackgroundComponent
se ponovno renderira samo kada se promijeni svojstvo background
teme, a ColorComponent
se ponovno renderira samo kada se promijeni svojstvo color
. To izbjegava nepotrebna ponovna renderiranja kada se cijela vrijednost konteksta promijeni.
Obrazac 6: Odvajanje akcija od stanja
Za veće aplikacije, razmislite o odvajanju vrijednosti konteksta u dva različita konteksta: jedan za stanje i drugi za akcije (dispatch funkcije). To može poboljšati organizaciju koda i mogućnost testiranja.
Primjer: Lista zadataka s odvojenim kontekstima za stanje i akcije
// 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;
}
};
// Upotreba
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;
Ovo odvajanje omogućuje komponentama da se pretplate samo na kontekst koji im je potreban, smanjujući nepotrebna ponovna renderiranja. Također olakšava jedinično testiranje reducera i svake komponente zasebno. Također, redoslijed omatanja providerima je bitan. ActionProvider
mora omotati StateProvider
.
Najbolje prakse i razmatranja
- Kontekst ne bi trebao zamijeniti sve biblioteke za upravljanje stanjem: Za vrlo velike i složene aplikacije, posvećene biblioteke za upravljanje stanjem poput Reduxa ili Zustanda i dalje bi mogle biti bolji izbor.
- Izbjegavajte pretjeranu upotrebu konteksta: Ne treba svaki dio stanja biti u kontekstu. Koristite kontekst razborito za doista globalno ili široko dijeljeno stanje.
- Testiranje performansi: Uvijek mjerite utjecaj upotrebe konteksta na performanse, posebno kada se radi o stanju koje se često ažurira.
- Code Splitting: Kada koristite Context API, razmislite o dijeljenju koda (code-splitting) vaše aplikacije na manje dijelove. Ovo je posebno važno kada mala promjena u stanju uzrokuje ponovno renderiranje velikog dijela aplikacije.
Zaključak
React Context API je svestran alat za upravljanje stanjem. Razumijevanjem i primjenom ovih naprednih obrazaca možete učinkovito upravljati složenim stanjem, optimizirati performanse i graditi održivije i skalabilnije React aplikacije. Ne zaboravite odabrati pravi obrazac za vaše specifične potrebe i pažljivo razmotriti implikacije performansi vaše upotrebe konteksta.
Kako se React razvija, tako će se razvijati i najbolje prakse vezane uz Context API. Ostanite informirani o novim tehnikama i bibliotekama kako biste bili spremni nositi se s izazovima upravljanja stanjem u modernom web razvoju. Razmislite o istraživanju novih obrazaca poput korištenja konteksta sa signalima za još finiju reaktivnost.