Lietuvių

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 ( ); };
// 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:

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 (
setText(e.target.value)} />
); } 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:

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 (
setText(e.target.value)} />
); } 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

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.

Pažangūs React Context API modeliai būsenos valdymui | MLOG