Čeština

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

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

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

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.