Polski

Poznaj zaawansowane wzorce dostawców React Context, aby efektywnie zarządzać stanem, optymalizować wydajność i zapobiegać niepotrzebnym ponownym renderowaniom w aplikacjach.

Wzorce dostawców React Context: Optymalizacja wydajności i unikanie problemów z ponownym renderowaniem

React Context API to potężne narzędzie do zarządzania globalnym stanem w aplikacjach. Pozwala dzielić dane między komponenty bez konieczności ręcznego przekazywania propsów na każdym poziomie. Jednak nieprawidłowe użycie Context może prowadzić do problemów z wydajnością, w szczególności niepotrzebnych ponownych renderowań. Ten artykuł omawia różne wzorce dostawców Context, które pomogą Ci zoptymalizować wydajność i uniknąć tych pułapek.

Zrozumienie problemu: Niepotrzebne ponowne renderowania

Domyślnie, gdy wartość Context się zmienia, wszystkie komponenty korzystające z tego Context zostaną ponownie wyrenderowane, nawet jeśli nie zależą od konkretnej części Context, która się zmieniła. Może to stanowić znaczący wąskie gardło wydajności, zwłaszcza w dużych i złożonych aplikacjach. Rozważ scenariusz, w którym masz Context zawierający informacje o użytkowniku, ustawienia motywu i preferencje aplikacji. Jeśli zmieni się tylko ustawienie motywu, idealnie tylko komponenty związane z motywami powinny się ponownie wyrenderować, a nie cała aplikacja.

Dla ilustracji wyobraź sobie globalną aplikację e-commerce dostępną w wielu krajach. Jeśli zmieni się preferencja waluty (obsłużona w ramach Context), nie chcesz, aby cały katalog produktów był ponownie renderowany – wystarczy zaktualizować wyświetlanie cen.

Wzorzec 1: Memoizacja wartości za pomocą useMemo

Najprostszym sposobem zapobiegania niepotrzebnym ponownym renderowaniom jest memoizacja wartości Context za pomocą useMemo. Zapewnia to, że wartość Context zmienia się tylko wtedy, gdy zmieniają się jej zależności.

Przykład:

Załóżmy, że mamy UserContext, który dostarcza dane użytkownika i funkcję do aktualizacji profilu użytkownika.


import React, { createContext, useState, useMemo } from 'react';

const UserContext = createContext(null);

function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john.doe@example.com',
    location: 'New York, USA'
  });

  const updateUser = (newUserData) => {
    setUser(prevState => ({ ...prevState, ...newUserData }));
  };

  const contextValue = useMemo(() => ({
    user,
    updateUser,
  }), [user, setUser]);

  return (
    
      {children}
    
  );
}

export { UserContext, UserProvider };

W tym przykładzie useMemo zapewnia, że contextValue zmienia się tylko wtedy, gdy zmienia się stan user lub funkcja setUser. Jeśli żaden z nich się nie zmieni, komponenty korzystające z UserContext nie zostaną ponownie wyrenderowane.

Zalety:

Wady:

Wzorzec 2: Rozdzielenie odpowiedzialności za pomocą wielu kontekstów

Bardziej granularne podejście polega na podziale Context na wiele mniejszych Contextów, z których każdy odpowiada za konkretny fragment stanu. Zmniejsza to zakres ponownych renderowań i zapewnia, że komponenty są ponownie renderowane tylko wtedy, gdy zmieniają się konkretne dane, od których zależą.

Przykład:

Zamiast pojedynczego UserContext, możemy stworzyć oddzielne konteksty dla danych użytkownika i preferencji użytkownika.


import React, { createContext, useState } from 'react';

const UserDataContext = createContext(null);
const UserPreferencesContext = createContext(null);

function UserDataProvider({ children }) {
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john.doe@example.com',
    location: 'New York, USA'
  });

  const updateUser = (newUserData) => {
    setUser(prevState => ({ ...prevState, ...newUserData }));
  };

  return (
    
      {children}
    
  );
}

function UserPreferencesProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [language, setLanguage] = useState('en');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    
      {children}
    
  );
}

export { UserDataContext, UserDataProvider, UserPreferencesContext, UserPreferencesProvider };

Teraz komponenty, które potrzebują tylko danych użytkownika, mogą korzystać z UserDataContext, a komponenty, które potrzebują tylko ustawień motywu, mogą korzystać z UserPreferencesContext. Zmiany w motywie nie spowodują już ponownego renderowania komponentów korzystających z UserDataContext i odwrotnie.

Zalety:

Wady:

Wzorzec 3: Funkcje selektorów z niestandardowymi hookami

Ten wzorzec polega na tworzeniu niestandardowych hooków, które wyodrębniają określone części wartości Context i ponownie renderują się tylko wtedy, gdy te konkretne części się zmieniają. Jest to szczególnie przydatne, gdy masz dużą wartość Context z wieloma właściwościami, ale komponent potrzebuje tylko kilku z nich.

Przykład:

Korzystając z oryginalnego UserContext, możemy utworzyć niestandardowe hooki do wybierania określonych właściwości użytkownika.


import React, { useContext } from 'react';
import { UserContext } from './UserContext'; // Zakładając, że UserContext znajduje się w UserContext.js

function useUserName() {
  const { user } = useContext(UserContext);
  return user.name;
}

function useUserEmail() {
  const { user } = useContext(UserContext);
  return user.email;
}

export { useUserName, useUserEmail };

Teraz komponent może używać useUserName do ponownego renderowania tylko wtedy, gdy zmienia się imię użytkownika, oraz useUserEmail do ponownego renderowania tylko wtedy, gdy zmienia się adres e-mail użytkownika. Zmiany w innych właściwościach użytkownika (np. lokalizacji) nie będą wywoływać ponownych renderowań.


import React from 'react';
import { useUserName, useUserEmail } from './UserHooks';

function UserProfile() {
  const name = useUserName();
  const email = useUserEmail();

  return (
    

Imię: {name}

Email: {email}

); }

Zalety:

Wady:

Wzorzec 4: Memoizacja komponentów za pomocą React.memo

React.memo to wyższy komponent funkcyjny (HOC), który memoizuje komponent funkcyjny. Zapobiega ponownemu renderowaniu komponentu, jeśli jego propsy się nie zmieniły. Możesz to połączyć z Context, aby dalej optymalizować wydajność.

Przykład:

Załóżmy, że mamy komponent wyświetlający imię użytkownika.


import React, { useContext } from 'react';
import { UserContext } from './UserContext';

function UserName() {
  const { user } = useContext(UserContext);
  return 

Imię: {user.name}

; } export default React.memo(UserName);

Opakowując UserName w React.memo, zostanie on ponownie wyrenderowany tylko wtedy, gdy zmieni się prop user (przekazywany niejawnie poprzez Context). Jednak w tym uproszczonym przykładzie samo React.memo nie zapobiegnie ponownym renderowaniom, ponieważ cały obiekt user jest nadal przekazywany jako prop. Aby uczynić go naprawdę skutecznym, musisz połączyć go z funkcjami selektorów lub oddzielnymi kontekstami.

Bardziej efektywny przykład łączy React.memo z funkcjami selektorów:


import React from 'react';
import { useUserName } from './UserHooks';

function UserName() {
  const name = useUserName();
  return 

Imię: {name}

; } function areEqual(prevProps, nextProps) { // Niestandardowa funkcja porównania return prevProps.name === nextProps.name; } export default React.memo(UserName, areEqual);

Tutaj areEqual to niestandardowa funkcja porównania, która sprawdza, czy zmienił się prop name. Jeśli się nie zmienił, komponent nie zostanie ponownie wyrenderowany.

Zalety:

Wady:

Wzorzec 5: Łączenie Context i Reducerów (useReducer)

Łączenie Context z useReducer pozwala zarządzać złożoną logiką stanu i optymalizować ponowne renderowania. useReducer zapewnia przewidywalny wzorzec zarządzania stanem i pozwala aktualizować stan na podstawie akcji, zmniejszając potrzebę przekazywania wielu funkcji ustawiających poprzez Context.

Przykład:


import React, { createContext, useReducer, useContext } from 'react';

const UserContext = createContext(null);

const initialState = {
  user: {
    name: 'John Doe',
    email: 'john.doe@example.com',
    location: 'New York, USA'
  },
  theme: 'light',
  language: 'en'
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_USER':
      return { ...state, user: { ...state.user, ...action.payload } };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    case 'SET_LANGUAGE':
      return { ...state, language: action.payload };
    default:
      return state;
  }
};

function UserProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    
      {children}
    
  );
}

function useUserState() {
  const { state } = useContext(UserContext);
  return state.user;
}

function useUserDispatch() {
    const { dispatch } = useContext(UserContext);
    return dispatch;
}


export { UserContext, UserProvider, useUserState, useUserDispatch };

Teraz komponenty mogą uzyskiwać dostęp do stanu i wysyłać akcje za pomocą niestandardowych hooków. Na przykład:


import React from 'react';
import { useUserState, useUserDispatch } from './UserContext';

function UserProfile() {
  const user = useUserState();
  const dispatch = useUserDispatch();

  const handleUpdateName = (e) => {
    dispatch({ type: 'UPDATE_USER', payload: { name: e.target.value } });
  };

  return (
    

Imię: {user.name}

); }

Ten wzorzec promuje bardziej ustrukturyzowane podejście do zarządzania stanem i może uprościć złożoną logikę Context.

Zalety:

Wady:

Wzorzec 6: Aktualizacje optymistyczne

Aktualizacje optymistyczne polegają na natychmiastowym aktualizowaniu interfejsu użytkownika, tak jakby akcja zakończyła się sukcesem, nawet zanim serwer ją potwierdzi. Może to znacząco poprawić doświadczenie użytkownika, zwłaszcza w sytuacjach z wysokim opóźnieniem. Wymaga jednak starannego obsługi potencjalnych błędów.

Przykład:

Wyobraź sobie aplikację, w której użytkownicy mogą polubić posty. Aktualizacja optymistyczna natychmiast zwiększyłaby liczbę polubień, gdy użytkownik kliknie przycisk polubienia, a następnie cofnęła zmianę, jeśli żądanie serwera zakończy się niepowodzeniem.


import React, { useContext, useState } from 'react';
import { UserContext } from './UserContext';

function LikeButton({ postId }) {
  const { dispatch } = useContext(UserContext);
  const [isLiking, setIsLiking] = useState(false);

  const handleLike = async () => {
    setIsLiking(true);
    // Optymistycznie zaktualizuj liczbę polubień
    dispatch({ type: 'INCREMENT_LIKES', payload: { postId } });

    try {
      // Symuluj wywołanie API
      await new Promise(resolve => setTimeout(resolve, 500));

      // Jeśli wywołanie API zakończy się sukcesem, nic nie rób (interfejs użytkownika jest już zaktualizowany)
    } catch (error) {
      // Jeśli wywołanie API zakończy się niepowodzeniem, cofnij optymistyczną aktualizację
      dispatch({ type: 'DECREMENT_LIKES', payload: { postId } });
      alert('Nie udało się polubić posta. Spróbuj ponownie.');
    } finally {
      setIsLiking(false);
    }
  };

  return (
    
  );
}

W tym przykładzie akcja INCREMENT_LIKES jest wysyłana natychmiast, a następnie cofana, jeśli wywołanie API zakończy się niepowodzeniem. Zapewnia to bardziej responsywne wrażenia użytkownika.

Zalety:

Wady:

Wybór odpowiedniego wzorca

Najlepszy wzorzec dostawcy Context zależy od specyficznych potrzeb Twojej aplikacji. Oto podsumowanie, które pomoże Ci wybrać:

Dodatkowe wskazówki dotyczące optymalizacji wydajności Context

Wniosek

React Context API to potężne narzędzie, ale ważne jest, aby używać go poprawnie, aby uniknąć problemów z wydajnością. Rozumiejąc i stosując omówione w tym artykule wzorce dostawców Context, możesz efektywnie zarządzać stanem, optymalizować wydajność oraz tworzyć bardziej wydajne i responsywne aplikacje React. Pamiętaj, aby analizować swoje specyficzne potrzeby i wybrać wzorzec, który najlepiej odpowiada wymaganiom Twojej aplikacji.

Biorąc pod uwagę globalną perspektywę, deweloperzy powinni również zapewnić, że rozwiązania do zarządzania stanem działają płynnie we wszystkich różnych strefach czasowych, formatach walut i regionalnych wymaganiach dotyczących danych. Na przykład funkcja formatowania daty w ramach Context powinna być lokalizowana na podstawie preferencji lub lokalizacji użytkownika, zapewniając spójne i dokładne wyświetlanie dat niezależnie od tego, gdzie użytkownik uzyskuje dostęp do aplikacji.