Română

Explorați pattern-uri avansate pentru React Context API, inclusiv componente compuse, contexte dinamice și tehnici de optimizare a performanței pentru managementul stării complexe.

Pattern-uri Avansate ale React Context API pentru Managementul Stării

React Context API oferă un mecanism puternic pentru partajarea stării în întreaga aplicație fără prop drilling. Deși utilizarea de bază este simplă, valorificarea întregului său potențial necesită înțelegerea unor pattern-uri avansate care pot gestiona scenarii complexe de management al stării. Acest articol explorează câteva dintre aceste pattern-uri, oferind exemple practice și perspective acționabile pentru a vă îmbunătăți dezvoltarea în React.

Înțelegerea Limitărilor API-ului de Context de Bază

Înainte de a explora pattern-urile avansate, este crucial să recunoaștem limitările API-ului de Context de bază. Deși este potrivit pentru o stare simplă, accesibilă global, poate deveni greoi și ineficient pentru aplicații complexe cu stări care se schimbă frecvent. Fiecare componentă care consumă un context se re-randează ori de câte ori valoarea contextului se schimbă, chiar dacă componenta nu se bazează pe partea specifică a stării care a fost actualizată. Acest lucru poate duce la blocaje de performanță.

Pattern 1: Componente Compuse cu Context

Pattern-ul Componentelor Compuse îmbunătățește Context API-ul prin crearea unei suite de componente înrudite care partajează implicit starea și logica printr-un context. Acest pattern promovează reutilizarea și simplifică API-ul pentru consumatori. Acest lucru permite ca logica complexă să fie încapsulată printr-o implementare simplă.

Exemplu: O Componentă de Tab-uri

Să ilustrăm acest lucru cu o componentă de Tab-uri. În loc să transmitem props-uri prin mai multe niveluri, componentele Tab comunică implicit printr-un context partajat.

// 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 ( ); };
// Utilizare
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Tab 1
        Tab 2
        Tab 3
      
      Conținut pentru Tab 1
      Conținut pentru Tab 2
      Conținut pentru Tab 3
    
  );
}

export default App;

Beneficii:

Pattern 2: Contexte Dinamice

În unele scenarii, este posibil să aveți nevoie de valori de context diferite în funcție de poziția componentei în arborele de componente sau de alți factori dinamici. Contextele dinamice vă permit să creați și să furnizați valori de context care variază în funcție de condiții specifice.

Exemplu: Teme cu Contexte Dinamice

Luați în considerare un sistem de teme în care doriți să oferiți teme diferite în funcție de preferințele utilizatorului sau de secțiunea aplicației în care se află. Putem crea un exemplu simplificat cu o temă deschisă și una închisă.

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

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

  return (
    

Aceasta este o componentă cu temă.

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

În acest exemplu, ThemeProvider determină dinamic tema pe baza stării isDarkTheme. Componentele care utilizează hook-ul useTheme se vor re-randa automat atunci când tema se schimbă.

Pattern 3: Context cu useReducer pentru Stări Complexe

Pentru gestionarea logicii complexe a stării, combinarea Context API cu useReducer este o abordare excelentă. useReducer oferă o modalitate structurată de a actualiza starea pe baza acțiunilor, iar Context API vă permite să partajați această stare și funcția de dispatch în întreaga aplicație.

Exemplu: O Listă Simplă de Sarcini (Todo)

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

Acest pattern centralizează logica de management al stării în cadrul reducer-ului, făcând-o mai ușor de înțeles și de testat. Componentele pot trimite acțiuni pentru a actualiza starea fără a fi nevoie să gestioneze direct starea.

Pattern 4: Actualizări de Context Optimizate cu `useMemo` și `useCallback`

După cum s-a menționat anterior, o considerație cheie de performanță cu Context API sunt re-randările inutile. Utilizarea useMemo și useCallback poate preveni aceste re-randări, asigurându-se că doar părțile necesare ale valorii contextului sunt actualizate și că referințele funcțiilor rămân stabile.

Exemplu: Optimizarea unui Context de Temă

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

Explicație:

Fără useCallback, funcția toggleTheme ar fi recreată la fiecare randare a ThemeProvider, determinând schimbarea value și declanșând re-randări în orice componentă consumatoare, chiar dacă tema în sine nu s-a schimbat. useMemo asigură că o nouă value este creată doar atunci când dependențele sale (theme sau toggleTheme) se schimbă.

Pattern 5: Selectori de Context

Selectorii de context permit componentelor să se aboneze doar la anumite părți ale valorii contextului. Acest lucru previne re-randările inutile atunci când alte părți ale contextului se schimbă. Biblioteci precum `use-context-selector` sau implementări personalizate pot fi folosite pentru a realiza acest lucru.

Exemplu Utilizând un Selector de Context Personalizat

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

    // În mod normal, aici v-ați abona la modificările contextului. Deoarece acesta este un exemplu
    // simplificat, vom apela imediat `subscription` pentru a inițializa.
    subscription();

    return () => {
      didUnmount = true;
      // Anulați abonarea la modificările contextului aici, dacă este cazul.
    };
  }, [value]); // Re-executați efectul ori de câte ori valoarea contextului se schimbă

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Simplificat pentru concizie)
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;
// Utilizare
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Fundal
; } function ColorComponent() { const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color); return
Culoare
; } 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;

În acest exemplu, BackgroundComponent se re-randează doar atunci când proprietatea background a temei se schimbă, iar ColorComponent se re-randează doar atunci când proprietatea color se schimbă. Acest lucru evită re-randările inutile atunci când întreaga valoare a contextului se modifică.

Pattern 6: Separarea Acțiunilor de Stare

Pentru aplicații mai mari, luați în considerare separarea valorii contextului în două contexte distincte: unul pentru stare și altul pentru acțiuni (funcțiile de dispatch). Acest lucru poate îmbunătăți organizarea codului și testabilitatea.

Exemplu: Listă de Sarcini cu Contexte Separate pentru Stare și Acțiuni

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

Această separare permite componentelor să se aboneze doar la contextul de care au nevoie, reducând re-randările inutile. De asemenea, facilitează testarea unitară a reducer-ului și a fiecărei componente în mod izolat. De asemenea, ordinea în care sunt încapsulați provider-ii contează. ActionProvider trebuie să încapsuleze StateProvider.

Bune Practici și Considerații

Concluzie

React Context API este un instrument versatil pentru managementul stării. Prin înțelegerea și aplicarea acestor pattern-uri avansate, puteți gestiona eficient stări complexe, optimiza performanța și construi aplicații React mai ușor de întreținut și scalabile. Amintiți-vă să alegeți pattern-ul potrivit pentru nevoile dvs. specifice și să luați în considerare cu atenție implicațiile de performanță ale utilizării contextului.

Pe măsură ce React evoluează, la fel se vor schimba și bunele practici legate de Context API. Rămânând informat despre noile tehnici și biblioteci vă va asigura că sunteți echipat pentru a face față provocărilor managementului stării în dezvoltarea web modernă. Luați în considerare explorarea pattern-urilor emergente, cum ar fi utilizarea contextului cu semnale (signals) pentru o reactivitate și mai granulară.