Slovenščina

Raziščite napredne vzorce za React Context API, vključno s sestavljenimi komponentami, dinamičnimi konteksti in tehnikami za optimizacijo delovanja.

Napredni vzorci React Context API za upravljanje stanja

React Context API ponuja zmogljiv mehanizem za deljenje stanja po celotni aplikaciji brez potrebe po "prop drilling-u". Medtem ko je osnovna uporaba preprosta, je za izkoriščanje njegovega polnega potenciala potrebno razumevanje naprednih vzorcev, ki lahko obvladajo kompleksne scenarije upravljanja stanja. Ta članek raziskuje več takšnih vzorcev, ponuja praktične primere in uporabne vpoglede za izboljšanje vašega razvoja v Reactu.

Razumevanje omejitev osnovnega Context API-ja

Preden se poglobimo v napredne vzorce, je ključnega pomena, da se zavedamo omejitev osnovnega Context API-ja. Čeprav je primeren za preprosto, globalno dostopno stanje, lahko postane okoren in neučinkovit pri kompleksnih aplikacijah s pogosto spreminjajočim se stanjem. Vsaka komponenta, ki uporablja kontekst, se ponovno renderira, kadar koli se vrednost konteksta spremeni, tudi če se komponenta ne zanaša na tisti specifični del stanja, ki je bil posodobljen. To lahko privede do ozkih grl v delovanju.

Vzorec 1: Sestavljene komponente s kontekstom

Vzorec sestavljenih komponent (Compound Component) izboljša Context API z ustvarjanjem sklopa povezanih komponent, ki si implicitno delijo stanje in logiko prek konteksta. Ta vzorec spodbuja ponovno uporabnost in poenostavlja API za uporabnike. To omogoča, da se kompleksna logika zapre v preprosto implementacijo.

Primer: Komponenta za zavihke

Poglejmo si primer komponente za zavihke (Tab component). Namesto posredovanja rekvizitov (props) skozi več plasti, komponente Tab implicitno komunicirajo prek deljenega 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 ( ); };
// Uporaba
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Tab 1
        Tab 2
        Tab 3
      
      Vsebina za zavihek 1
      Vsebina za zavihek 2
      Vsebina za zavihek 3
    
  );
}

export default App;

Prednosti:

Vzorec 2: Dinamični konteksti

V nekaterih primerih boste morda potrebovali različne vrednosti konteksta glede na položaj komponente v drevesu komponent ali druge dinamične dejavnike. Dinamični konteksti vam omogočajo ustvarjanje in zagotavljanje vrednosti konteksta, ki se razlikujejo glede na specifične pogoje.

Primer: Teme z dinamičnimi konteksti

Predstavljajte si sistem za teme, kjer želite ponuditi različne teme glede na uporabnikove preference ali del aplikacije, v katerem se nahaja. Lahko naredimo poenostavljen primer s svetlo in temno temo.

// 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);
};
// Uporaba
import { useTheme, ThemeProvider } from './ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useTheme();

  return (
    

To je komponenta s temo.

); } function App() { return ( ); } export default App;

V tem primeru ThemeProvider dinamično določa temo glede na stanje isDarkTheme. Komponente, ki uporabljajo kavelj useTheme, se bodo samodejno ponovno renderirale, ko se tema spremeni.

Vzorec 3: Kontekst z useReducer za kompleksna stanja

Za upravljanje kompleksne logike stanja je kombiniranje Context API-ja s kavljem useReducer odličen pristop. useReducer ponuja strukturiran način za posodabljanje stanja na podlagi akcij, Context API pa omogoča deljenje tega stanja in funkcije `dispatch` po celotni aplikaciji.

Primer: Enostaven seznam opravil

// 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;
};
// Uporaba
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;

Ta vzorec centralizira logiko upravljanja stanja znotraj reducerja, kar olajša razumevanje in testiranje. Komponente lahko pošiljajo akcije za posodobitev stanja, ne da bi morale same neposredno upravljati s stanjem.

Vzorec 4: Optimizirane posodobitve konteksta z `useMemo` in `useCallback`

Kot smo že omenili, je ključni vidik delovanja pri Context API-ju nepotrebno ponovno renderiranje. Uporaba kavljev useMemo in useCallback lahko prepreči ta ponovna renderiranja, saj zagotavlja, da se posodobijo le potrebni deli vrednosti konteksta in da reference funkcij ostanejo stabilne.

Primer: Optimizacija konteksta za temo

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

Pojasnilo:

Brez useCallback bi se funkcija toggleTheme ponovno ustvarila ob vsakem renderiranju ThemeProvider-ja, kar bi povzročilo spremembo value in sprožilo ponovna renderiranja v vseh komponentah, ki uporabljajo kontekst, tudi če se tema sama ni spremenila. useMemo zagotavlja, da se nova value ustvari le, ko se spremenijo njene odvisnosti (theme ali toggleTheme).

Vzorec 5: Selektorji konteksta

Selektorji konteksta omogočajo komponentam, da se naročijo le na določene dele vrednosti konteksta. To preprečuje nepotrebna ponovna renderiranja, ko se spremenijo drugi deli konteksta. Za dosego tega lahko uporabimo knjižnice, kot je `use-context-selector`, ali implementacije po meri.

Primer uporabe selektorja konteksta po meri

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

    // Ponavadi bi se tukaj naročili na spremembe konteksta. Ker je to poenostavljen
    // primer, bomo funkcijo subscription poklicali takoj za inicializacijo.
    subscription();

    return () => {
      didUnmount = true;
      // Tukaj bi se odjavili od sprememb konteksta, če je to potrebno.
    };
  }, [value]); // Ponovno zaženi učinek, kadarkoli se vrednost konteksta spremeni

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (poenostavljeno za kratkost)
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;
// Uporaba
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Ozadje
; } 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 tem primeru se BackgroundComponent ponovno renderira samo, ko se spremeni lastnost background teme, in ColorComponent se ponovno renderira samo, ko se spremeni lastnost color. S tem se izognemo nepotrebnim ponovnim renderiranjem, ko se spremeni celotna vrednost konteksta.

Vzorec 6: Ločevanje akcij od stanja

Pri večjih aplikacijah razmislite o ločitvi vrednosti konteksta na dva ločena konteksta: enega za stanje in drugega za akcije (dispatch funkcije). To lahko izboljša organizacijo kode in testiranje.

Primer: Seznam opravil z ločenima kontekstoma za stanje in 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;
  }
};
// Uporaba
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;

Ta ločitev omogoča komponentam, da se naročijo samo na kontekst, ki ga potrebujejo, kar zmanjša nepotrebno ponovno renderiranje. Prav tako olajša enotno testiranje reducerja in vsake komponente posebej. Pomemben je tudi vrstni red ovojev providerjev. ActionProvider mora ovijati StateProvider.

Najboljše prakse in priporočila

Zaključek

React Context API je vsestransko orodje za upravljanje stanja. Z razumevanjem in uporabo teh naprednih vzorcev lahko učinkovito upravljate kompleksna stanja, optimizirate delovanje ter gradite bolj vzdrževane in razširljive aplikacije React. Ne pozabite izbrati pravega vzorca za svoje specifične potrebe in skrbno pretehtati posledic uporabe konteksta na delovanje.

Z razvojem Reacta se bodo razvijale tudi najboljše prakse v zvezi s Context API-jem. Z obveščenostjo o novih tehnikah in knjižnicah boste opremljeni za soočanje z izzivi upravljanja stanja v sodobnem spletnem razvoju. Razmislite o raziskovanju nastajajočih vzorcev, kot je uporaba konteksta s signali za še bolj natančno reaktivnost.