Hrvatski

Istražite napredne obrasce za React Context API, uključujući složene komponente, dinamičke kontekste i tehnike optimizacije performansi za složeno upravljanje stanjem.

Napredni obrasci React Context API-ja za upravljanje stanjem

React Context API pruža moćan mehanizam za dijeljenje stanja kroz aplikaciju bez potrebe za "prop drillingom". Iako je osnovna upotreba jednostavna, iskorištavanje punog potencijala zahtijeva razumijevanje naprednih obrazaca koji mogu rješavati složene scenarije upravljanja stanjem. Ovaj članak istražuje nekoliko tih obrazaca, nudeći praktične primjere i korisne uvide za podizanje vašeg React razvoja na višu razinu.

Razumijevanje ograničenja osnovnog Context API-ja

Prije nego što zaronimo u napredne obrasce, ključno je prepoznati ograničenja osnovnog Context API-ja. Iako je prikladan za jednostavno, globalno dostupno stanje, može postati nespretan i neučinkovit za složene aplikacije s često mijenjajućim stanjem. Svaka komponenta koja koristi kontekst ponovno se renderira svaki put kad se vrijednost konteksta promijeni, čak i ako komponenta ne ovisi o specifičnom dijelu stanja koji je ažuriran. To može dovesti do uskih grla u performansama.

Obrazac 1: Složene komponente (Compound Components) s kontekstom

Obrazac složenih komponenata (Compound Component) poboljšava Context API stvaranjem skupa povezanih komponenata koje implicitno dijele stanje i logiku putem konteksta. Ovaj obrazac promiče ponovnu upotrebljivost i pojednostavljuje API za korisnike. To omogućuje da se složena logika enkapsulira jednostavnom implementacijom.

Primjer: Komponenta s karticama (Tab)

Ilustrirajmo to s komponentom za kartice (Tab). Umjesto prosljeđivanja propsa kroz više slojeva, Tab komponente implicitno komuniciraju putem zajedničkog 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 ( ); };
// Upotreba
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;

Prednosti:

Obrazac 2: Dinamički konteksti

U nekim scenarijima, možda će vam trebati različite vrijednosti konteksta ovisno o poziciji komponente u stablu komponenata ili drugim dinamičkim faktorima. Dinamički konteksti omogućuju vam stvaranje i pružanje vrijednosti konteksta koje variraju ovisno o specifičnim uvjetima.

Primjer: Teme s dinamičkim kontekstima

Razmotrimo sustav tema gdje želite pružiti različite teme ovisno o korisničkim postavkama ili dijelu aplikacije u kojem se nalaze. Možemo napraviti pojednostavljeni primjer sa svijetlom i tamnom temom.

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

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

  return (
    

This is a themed component.

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

U ovom primjeru, ThemeProvider dinamički određuje temu na temelju stanja isDarkTheme. Komponente koje koriste hook useTheme automatski će se ponovno renderirati kada se tema promijeni.

Obrazac 3: Kontekst s useReducerom za složeno stanje

Za upravljanje složenom logikom stanja, kombiniranje Context API-ja s useReducer-om je izvrstan pristup. useReducer pruža strukturiran način ažuriranja stanja na temelju akcija, a Context API omogućuje dijeljenje tog stanja i dispatch funkcije kroz cijelu aplikaciju.

Primjer: Jednostavna lista zadataka (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;
};
// Upotreba
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;

Ovaj obrazac centralizira logiku upravljanja stanjem unutar reducera, što olakšava razumijevanje i testiranje. Komponente mogu slati (dispatch) akcije za ažuriranje stanja bez potrebe da izravno upravljaju stanjem.

Obrazac 4: Optimizirana ažuriranja konteksta s `useMemo` i `useCallback`

Kao što je ranije spomenuto, ključna stavka performansi kod Context API-ja su nepotrebna ponovna renderiranja. Korištenje useMemo i useCallback može spriječiti ta ponovna renderiranja osiguravajući da se ažuriraju samo potrebni dijelovi vrijednosti konteksta i da reference funkcija ostanu stabilne.

Primjer: Optimizacija konteksta za teme

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

Objašnjenje:

Bez useCallback, funkcija toggleTheme bila bi ponovno stvorena pri svakom renderiranju ThemeProvider-a, što bi uzrokovalo promjenu value i pokrenulo ponovna renderiranja u svim komponentama koje koriste kontekst, čak i ako se sama tema nije promijenila. useMemo osigurava da se nova value stvara samo kada se promijene njezine ovisnosti (theme ili toggleTheme).

Obrazac 5: Selektori konteksta

Selektori konteksta omogućuju komponentama da se pretplate samo na specifične dijelove vrijednosti konteksta. To sprječava nepotrebna ponovna renderiranja kada se drugi dijelovi konteksta promijene. Za postizanje ovoga mogu se koristiti biblioteke poput `use-context-selector` ili prilagođene implementacije.

Primjer korištenja prilagođenog selektora konteksta

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

    // Ovdje biste se obično pretplatili na promjene konteksta. Budući da je ovo pojednostavljeni
    // primjer, samo ćemo odmah pozvati subscription za inicijalizaciju.
    subscription();

    return () => {
      didUnmount = true;
      // Ovdje se odjavite s promjena konteksta, ako je primjenjivo.
    };
  }, [value]); // Ponovno pokreni efekt svaki put kad se vrijednost konteksta promijeni

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Pojednostavljeno radi sažetosti)
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;
// Upotreba
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;

U ovom primjeru, BackgroundComponent se ponovno renderira samo kada se promijeni svojstvo background teme, a ColorComponent se ponovno renderira samo kada se promijeni svojstvo color. To izbjegava nepotrebna ponovna renderiranja kada se cijela vrijednost konteksta promijeni.

Obrazac 6: Odvajanje akcija od stanja

Za veće aplikacije, razmislite o odvajanju vrijednosti konteksta u dva različita konteksta: jedan za stanje i drugi za akcije (dispatch funkcije). To može poboljšati organizaciju koda i mogućnost testiranja.

Primjer: Lista zadataka s odvojenim kontekstima za stanje i 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;
  }
};
// Upotreba
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;

Ovo odvajanje omogućuje komponentama da se pretplate samo na kontekst koji im je potreban, smanjujući nepotrebna ponovna renderiranja. Također olakšava jedinično testiranje reducera i svake komponente zasebno. Također, redoslijed omatanja providerima je bitan. ActionProvider mora omotati StateProvider.

Najbolje prakse i razmatranja

Zaključak

React Context API je svestran alat za upravljanje stanjem. Razumijevanjem i primjenom ovih naprednih obrazaca možete učinkovito upravljati složenim stanjem, optimizirati performanse i graditi održivije i skalabilnije React aplikacije. Ne zaboravite odabrati pravi obrazac za vaše specifične potrebe i pažljivo razmotriti implikacije performansi vaše upotrebe konteksta.

Kako se React razvija, tako će se razvijati i najbolje prakse vezane uz Context API. Ostanite informirani o novim tehnikama i bibliotekama kako biste bili spremni nositi se s izazovima upravljanja stanjem u modernom web razvoju. Razmislite o istraživanju novih obrazaca poput korištenja konteksta sa signalima za još finiju reaktivnost.