Dansk

Udforsk avancerede mønstre for React Context API, herunder compound components, dynamiske contexts og optimerede performance-teknikker for kompleks state management.

Avancerede React Context API Mønstre for State Management

React Context API'et tilbyder en kraftfuld mekanisme til at dele state på tværs af din applikation uden prop drilling. Selvom grundlæggende brug er ligetil, kræver det at udnytte dets fulde potentiale en forståelse af avancerede mønstre, der kan håndtere komplekse state management-scenarier. Denne artikel udforsker flere af disse mønstre og tilbyder praktiske eksempler og handlingsorienterede indsigter for at løfte din React-udvikling.

Forståelse af Begrænsningerne ved Grundlæggende Context API

Før vi dykker ned i avancerede mønstre, er det afgørende at anerkende begrænsningerne ved det grundlæggende Context API. Selvom det er velegnet til simpel, globalt tilgængelig state, kan det blive uhåndterligt og ineffektivt for komplekse applikationer med hyppigt skiftende state. Hver komponent, der forbruger en context, re-renderer, hver gang context-værdien ændres, selvom komponenten ikke er afhængig af den specifikke del af den state, der blev opdateret. Dette kan føre til performance-flaskehalse.

Mønster 1: Compound Components med Context

Compound Component-mønsteret udvider Context API'et ved at skabe en række relaterede komponenter, der implicit deler state og logik gennem en context. Dette mønster fremmer genanvendelighed og forenkler API'et for forbrugerne. Dette gør det muligt at indkapsle kompleks logik med en simpel implementering.

Eksempel: En Fanebladskomponent

Lad os illustrere dette med en fanebladskomponent. I stedet for at sende props ned gennem flere lag, kommunikerer Tab-komponenterne implicit gennem en delt context.

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

Fordele:

Mønster 2: Dynamiske Contexts

I nogle scenarier kan du have brug for forskellige context-værdier baseret på komponentens position i komponenttræet eller andre dynamiske faktorer. Dynamiske contexts giver dig mulighed for at oprette og levere context-værdier, der varierer baseret på specifikke betingelser.

Eksempel: Temastyring med Dynamiske Contexts

Overvej et temasystem, hvor du vil tilbyde forskellige temaer baseret på brugerens præferencer eller den del af applikationen, de befinder sig i. Vi kan lave et forenklet eksempel med et lyst og et mørkt 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);
};
// Usage
import { useTheme, ThemeProvider } from './ThemeContext';

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

  return (
    

This is a themed component.

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

I dette eksempel bestemmer ThemeProvider dynamisk temaet baseret på isDarkTheme-state. Komponenter, der bruger useTheme-hook'et, vil automatisk re-render, når temaet ændres.

Mønster 3: Context med useReducer for Kompleks State

Til håndtering af kompleks state-logik er kombinationen af Context API med useReducer en fremragende tilgang. useReducer giver en struktureret måde at opdatere state på baseret på actions, og Context API giver dig mulighed for at dele denne state og dispatch-funktion på tværs af din applikation.

Eksempel: En Simpel To-Do Liste

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

Dette mønster centraliserer state management-logikken i reduceren, hvilket gør den lettere at ræsonnere om og teste. Komponenter kan dispatche actions for at opdatere state uden selv at skulle håndtere den direkte.

Mønster 4: Optimerede Context-Opdateringer med `useMemo` og `useCallback`

Som tidligere nævnt er en vigtig performance-overvejelse med Context API unødvendige re-renders. Ved at bruge useMemo og useCallback kan man forhindre disse re-renders ved at sikre, at kun de nødvendige dele af context-værdien opdateres, og at funktionsreferencer forbliver stabile.

Eksempel: Optimering af en Theme Context

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

Forklaring:

Uden useCallback ville toggleTheme-funktionen blive genskabt ved hver render af ThemeProvider, hvilket ville få value til at ændre sig og udløse re-renders i alle forbrugende komponenter, selvom temaet i sig selv ikke havde ændret sig. useMemo sikrer, at en ny value kun oprettes, når dens afhængigheder (theme eller toggleTheme) ændres.

Mønster 5: Context Selectors

Context selectors giver komponenter mulighed for kun at abonnere på specifikke dele af context-værdien. Dette forhindrer unødvendige re-renders, når andre dele af contexten ændres. Biblioteker som `use-context-selector` eller custom implementeringer kan bruges til at opnå dette.

Eksempel med en Custom Context Selector

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

    // You would typically subscribe to context changes here. Since this is a simplified
    // example, we'll just call subscription immediately to initialize.
    subscription();

    return () => {
      didUnmount = true;
      // Unsubscribe from context changes here, if applicable.
    };
  }, [value]); // Re-run effect whenever the context value changes

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Simplified for brevity)
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;
// Usage
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;

I dette eksempel re-render BackgroundComponent kun, når background-egenskaben i temaet ændres, og ColorComponent re-render kun, når color-egenskaben ændres. Dette undgår unødvendige re-renders, når hele context-værdien ændres.

Mønster 6: Adskillelse af Actions fra State

For større applikationer kan man overveje at opdele context-værdien i to separate contexts: en for state og en anden for actions (dispatch-funktioner). Dette kan forbedre kodestruktur og testbarhed.

Eksempel: To-Do Liste med Separate State- og Action-Contexts

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

Denne adskillelse giver komponenter mulighed for kun at abonnere på den context, de har brug for, hvilket reducerer unødvendige re-renders. Det gør det også lettere at enhedsteste reduceren og hver komponent isoleret. Desuden er rækkefølgen af provider-wrapping vigtig. ActionProvider skal wrappe StateProvider.

Bedste Praksis og Overvejelser

Konklusion

React Context API er et alsidigt værktøj til state management. Ved at forstå og anvende disse avancerede mønstre kan du effektivt håndtere kompleks state, optimere performance og bygge mere vedligeholdelsesvenlige og skalerbare React-applikationer. Husk at vælge det rigtige mønster til dine specifikke behov og omhyggeligt overveje performance-konsekvenserne af din context-brug.

I takt med at React udvikler sig, vil bedste praksis omkring Context API også gøre det. At holde sig informeret om nye teknikker og biblioteker vil sikre, at du er rustet til at håndtere state management-udfordringerne i moderne webudvikling. Overvej at udforske nye mønstre som at bruge context med signals for endnu mere finkornet reaktivitet.