Atraskite pažangius React Context API modelius, įskaitant sudėtinius komponentus, dinaminius kontekstus ir optimizuotas našumo technikas sudėtingam būsenos valdymui.
Pažangūs React Context API modeliai būsenos valdymui
React Context API suteikia galingą mechanizmą būsenai dalytis visoje programoje, išvengiant „prop drilling“ (perteikimo per kelis lygius). Nors pagrindinis naudojimas yra paprastas, norint išnaudoti visą jo potencialą, reikia suprasti pažangius modelius, kurie gali valdyti sudėtingus būsenos valdymo scenarijus. Šiame straipsnyje nagrinėjami keli tokie modeliai, pateikiami praktiniai pavyzdžiai ir veiksmingos įžvalgos, kurios pagerins jūsų React kūrimo įgūdžius.
Pagrindinio Context API apribojimų supratimas
Prieš pradedant nagrinėti pažangius modelius, svarbu pripažinti pagrindinio Context API apribojimus. Nors jis tinka paprastai, globaliai pasiekiamai būsenai, jis gali tapti nepatogus ir neefektyvus sudėtingose programose su dažnai kintančia būsena. Kiekvienas komponentas, naudojantis kontekstą, persikrauna kaskart, kai pasikeičia konteksto reikšmė, net jei komponentas nepriklauso nuo tos konkrečios būsenos dalies, kuri buvo atnaujinta. Tai gali sukelti našumo problemų.
1 modelis: Sudėtiniai komponentai su kontekstu
Sudėtinių komponentų modelis pagerina Context API, sukuriant susijusių komponentų rinkinį, kurie netiesiogiai dalijasi būsena ir logika per kontekstą. Šis modelis skatina pakartotinį panaudojimą ir supaprastina API naudotojams. Tai leidžia sudėtingą logiką inkapsuliuoti paprastu įgyvendinimu.
Pavyzdys: Kortelių (Tab) komponentas
Iliustruokime tai su kortelių (Tab) komponentu. Užuot perdavus „props“ per kelis lygius, Tab
komponentai netiesiogiai bendrauja per bendrą kontekstą.
// 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}
);
};
// Naudojimas
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;
Privalumai:
- Supaprastintas API naudotojams: Vartotojams reikia rūpintis tik
Tab
,TabList
irTabPanel
. - Numanomas būsenos dalijimasis: Komponentai automatiškai pasiekia ir atnaujina bendrą būseną.
- Pagerintas pakartotinis panaudojamumas:
Tab
komponentą galima lengvai pakartotinai naudoti skirtinguose kontekstuose.
2 modelis: Dinaminiai kontekstai
Kai kuriais atvejais jums gali prireikti skirtingų konteksto reikšmių, priklausomai nuo komponento padėties komponentų medyje ar kitų dinaminių veiksnių. Dinaminiai kontekstai leidžia jums sukurti ir pateikti konteksto reikšmes, kurios kinta priklausomai nuo konkrečių sąlygų.
Pavyzdys: Temos kūrimas su dinaminiais kontekstais
Apsvarstykite temų sistemą, kurioje norite pateikti skirtingas temas, atsižvelgiant į vartotojo nuostatas ar programos dalį, kurioje jis yra. Galime sukurti supaprastintą pavyzdį su šviesia ir tamsia tema.
// 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);
};
// Naudojimas
import { useTheme, ThemeProvider } from './ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useTheme();
return (
This is a themed component.
);
}
function App() {
return (
);
}
export default App;
Šiame pavyzdyje ThemeProvider
dinamiškai nustato temą, remdamasis isDarkTheme
būsena. Komponentai, naudojantys useTheme
„hook“, automatiškai persikraus, kai pasikeis tema.
3 modelis: Kontekstas su useReducer sudėtingai būsenai
Sudėtingos būsenos logikos valdymui, Context API derinimas su useReducer
yra puikus metodas. useReducer
suteikia struktūrizuotą būdą atnaujinti būseną remiantis veiksmais, o Context API leidžia dalytis šia būsena ir išsiuntimo (dispatch) funkcija visoje programoje.
Pavyzdys: Paprastas užduočių sąrašas
// 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;
};
// Naudojimas
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;
Šis modelis centralizuoja būsenos valdymo logiką „reducer“ viduje, todėl ją lengviau suprasti ir testuoti. Komponentai gali siųsti veiksmus būsenai atnaujinti, nereikėdami tiesiogiai valdyti pačios būsenos.
4 modelis: Optimizuoti konteksto atnaujinimai su `useMemo` ir `useCallback`
Kaip minėta anksčiau, pagrindinis našumo aspektas naudojant Context API yra nereikalingi persikrovimai. Naudojant useMemo
ir useCallback
galima išvengti šių persikrovimų, užtikrinant, kad būtų atnaujinamos tik būtinos konteksto reikšmės dalys ir kad funkcijų nuorodos išliktų stabilios.
Pavyzdys: Temos konteksto optimizavimas
// 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);
};
Paaiškinimas:
useCallback
memoizuojatoggleTheme
funkciją. Tai užtikrina, kad funkcijos nuoroda pasikeistų tik tada, kai pasikeičiaisDarkTheme
, taip išvengiant nereikalingų komponentų, kurie priklauso tik nuotoggleTheme
funkcijos, persikrovimų.useMemo
memoizuoja konteksto reikšmę. Tai užtikrina, kad konteksto reikšmė pasikeistų tik tada, kai pasikeičia arbatheme
, arbatoggleTheme
funkcija, dar labiau išvengiant nereikalingų persikrovimų.
Be useCallback
, toggleTheme
funkcija būtų sukuriama iš naujo kiekvieną kartą, kai persikrauna ThemeProvider
, todėl pasikeistų value
ir būtų iššaukti persikrovimai visuose ją naudojančiuose komponentuose, net jei pati tema nepasikeitė. useMemo
užtikrina, kad nauja value
reikšmė būtų sukurta tik tada, kai pasikeičia jos priklausomybės (theme
arba toggleTheme
).
5 modelis: Konteksto selektoriai
Konteksto selektoriai leidžia komponentams prenumeruoti tik konkrečias konteksto reikšmės dalis. Tai apsaugo nuo nereikalingų persikrovimų, kai pasikeičia kitos konteksto dalys. Tam pasiekti galima naudoti bibliotekas, tokias kaip `use-context-selector`, arba pasirinktinius sprendimus.
Pavyzdys naudojant pasirinktinį konteksto selektorių
// 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);
}
};
// Čia paprastai prenumeruotumėte konteksto pakeitimus. Kadangi tai supaprastintas
// pavyzdys, mes tiesiog iškviesime prenumeratą iškart, kad ją inicializuotume.
subscription();
return () => {
didUnmount = true;
// Čia atsisakykite konteksto pakeitimų prenumeratos, jei taikoma.
};
}, [value]); // Iš naujo paleisti efektą, kai pasikeičia konteksto reikšmė
return selected;
}
export default useCustomContextSelector;
// ThemeContext.js (supaprastinta trumpumo dėlei)
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;
// Naudojimas
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;
Šiame pavyzdyje BackgroundComponent
persikrauna tik tada, kai pasikeičia temos background
savybė, o ColorComponent
persikrauna tik tada, kai pasikeičia color
savybė. Tai leidžia išvengti nereikalingų persikrovimų, kai pasikeičia visa konteksto reikšmė.
6 modelis: Veiksmų atskyrimas nuo būsenos
Didesnėse programose apsvarstykite galimybę padalinti konteksto reikšmę į du atskirus kontekstus: vieną būsenai ir kitą veiksmams (išsiuntimo funkcijoms). Tai gali pagerinti kodo organizavimą ir testuojamumą.
Pavyzdys: Užduočių sąrašas su atskirais būsenos ir veiksmų kontekstais
// 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;
}
};
// Naudojimas
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;
Šis atskyrimas leidžia komponentams prenumeruoti tik jiems reikalingą kontekstą, sumažinant nereikalingus persikrovimus. Taip pat lengviau atlikti vienetinius „reducer“ ir kiekvieno komponento testavimus atskirai. Be to, svarbi tiekėjų (provider) apgaubimo tvarka. ActionProvider
turi apgaubti StateProvider
.
Gerosios praktikos ir svarstymai
- Kontekstas neturėtų pakeisti visų būsenos valdymo bibliotekų: Labai didelėms ir sudėtingoms programoms specializuotos būsenos valdymo bibliotekos, tokios kaip Redux ar Zustand, vis tiek gali būti geresnis pasirinkimas.
- Venkite perteklinio konteksto naudojimo: Ne kiekviena būsenos dalis turi būti kontekste. Naudokite kontekstą apgalvotai, tikrai globaliai ar plačiai dalijamai būsenai.
- Našumo testavimas: Visada išmatuokite savo konteksto naudojimo poveikį našumui, ypač dirbant su dažnai atnaujinama būsena.
- Kodo skaidymas (Code Splitting): Naudojant Context API, apsvarstykite galimybę suskaidyti savo programą į mažesnes dalis. Tai ypač svarbu, kai nedidelis būsenos pakeitimas sukelia didelės programos dalies persikrovimą.
Išvados
React Context API yra universalus įrankis būsenos valdymui. Suprasdami ir taikydami šiuos pažangius modelius, galite efektyviai valdyti sudėtingą būseną, optimizuoti našumą ir kurti labiau prižiūrimas bei keičiamo dydžio React programas. Nepamirškite pasirinkti tinkamą modelį pagal savo konkrečius poreikius ir atidžiai apsvarstyti konteksto naudojimo poveikį našumui.
React vystantis, keisis ir gerosios praktikos, susijusios su Context API. Būdami informuoti apie naujas technikas ir bibliotekas, užtikrinsite, kad esate pasirengę spręsti šiuolaikinės žiniatinklio kūrimo būsenos valdymo iššūkius. Apsvarstykite galimybę tyrinėti naujus modelius, pavyzdžiui, konteksto naudojimą su signalais dar smulkesniam reaktyvumui pasiekti.