Norsk

Utforsk avanserte React Context Provider-mønstre for effektivt å håndtere tilstand, optimalisere ytelse og forhindre unødvendige ny renderinger i applikasjonene dine.

React Context Provider-mønstre: Optimalisering av ytelse og unngåelse av problemer med ny rendering

React Context API er et kraftig verktøy for å håndtere global tilstand i applikasjonene dine. Det lar deg dele data mellom komponenter uten å måtte sende props manuelt på alle nivåer. Men feil bruk av Context kan føre til ytelsesproblemer, spesielt unødvendige ny renderinger. Denne artikkelen utforsker forskjellige Context Provider-mønstre som hjelper deg med å optimalisere ytelsen og unngå disse fallgruvene.

Forstå problemet: Unødvendige ny renderinger

Som standard vil alle komponenter som bruker en Context, rendre på nytt når en Context-verdi endres, selv om de ikke er avhengige av den spesifikke delen av Context som er endret. Dette kan være en betydelig ytelsesflaskehals, spesielt i store og komplekse applikasjoner. Tenk deg et scenario der du har en Context som inneholder brukerinformasjon, temainnstillinger og applikasjonspreferanser. Hvis bare temainnstillingen endres, bør ideelt sett bare komponenter relatert til temaer rendre på nytt, ikke hele applikasjonen.

For å illustrere, tenk deg en global e-handelsapplikasjon som er tilgjengelig i flere land. Hvis valutapreferansen endres (håndtert i Context), vil du ikke at hele produktkatalogen skal rendre på nytt – bare prisvisningene trenger oppdatering.

Mønster 1: Verdi-memoisering med useMemo

Den enkleste måten å forhindre unødvendige ny renderinger er å memoisere Context-verdien ved hjelp av useMemo. Dette sikrer at Context-verdien bare endres når dens avhengigheter endres.

Eksempel:

La oss si at vi har en `UserContext` som gir brukerdata og en funksjon for å oppdatere brukerens profil.


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

I dette eksemplet sikrer useMemo at `contextValue` bare endres når `user`-tilstanden eller `setUser`-funksjonen endres. Hvis ingen av dem endres, vil ikke komponenter som bruker `UserContext` rendre på nytt.

Fordeler:

Ulemper:

Mønster 2: Separere ansvarsområder med flere Contexts

En mer granulær tilnærming er å dele Context inn i flere, mindre Contexts, hver ansvarlig for en spesifikk del av tilstanden. Dette reduserer omfanget av ny renderinger og sikrer at komponenter bare rendre på nytt når de spesifikke dataene de er avhengige av, endres.

Eksempel:

I stedet for en enkelt `UserContext`, kan vi opprette separate contexts for brukerdata og brukerpreferanser.


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

Nå kan komponenter som bare trenger brukerdata bruke `UserDataContext`, og komponenter som bare trenger temainnstillinger kan bruke `UserPreferencesContext`. Endringer i temaet vil ikke lenger føre til at komponenter som bruker `UserDataContext` rendre på nytt, og omvendt.

Fordeler:

Ulemper:

Mønster 3: Velgerfunksjoner med egendefinerte Hooks

Dette mønsteret innebærer å lage egendefinerte hooks som trekker ut spesifikke deler av Context-verdien og bare rendre på nytt når disse spesifikke delene endres. Dette er spesielt nyttig når du har en stor Context-verdi med mange egenskaper, men en komponent bare trenger noen få av dem.

Eksempel:

Ved hjelp av den originale `UserContext`, kan vi lage egendefinerte hooks for å velge spesifikke brukeregenskaper.


import React, { useContext } from 'react';
import { UserContext } from './UserContext'; // Assuming UserContext is in UserContext.js

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

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

export { useUserName, useUserEmail };

Nå kan en komponent bruke `useUserName` til å bare rendre på nytt når brukerens navn endres, og `useUserEmail` til å bare rendre på nytt når brukerens e-post endres. Endringer i andre brukeregenskaper (f.eks. plassering) vil ikke utløse ny renderinger.


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

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

  return (
    

Name: {name}

Email: {email}

); }

Fordeler:

Ulemper:

Mønster 4: Komponentmemoisering med React.memo

React.memo er en høyereordens komponent (HOC) som memoiserer en funksjonell komponent. Den hindrer komponenten fra å rendre på nytt hvis dens props ikke er endret. Du kan kombinere dette med Context for å optimalisere ytelsen ytterligere.

Eksempel:

La oss si at vi har en komponent som viser brukerens navn.


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

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

Name: {user.name}

; } export default React.memo(UserName);

Ved å pakke `UserName` med `React.memo`, vil den bare rendre på nytt hvis `user`-propen (sendt implisitt via Context) endres. Men i dette forenklede eksemplet vil ikke `React.memo` alene forhindre ny renderinger fordi hele `user`-objektet fortsatt sendes som en prop. For å gjøre det virkelig effektivt, må du kombinere det med velgerfunksjoner eller separate contexts.

Et mer effektivt eksempel kombinerer `React.memo` med velgerfunksjoner:


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

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

Name: {name}

; } function areEqual(prevProps, nextProps) { // Custom comparison function return prevProps.name === nextProps.name; } export default React.memo(UserName, areEqual);

Her er `areEqual` en egendefinert sammenligningsfunksjon som sjekker om `name`-propen er endret. Hvis den ikke har det, vil ikke komponenten rendre på nytt.

Fordeler:

Ulemper:

Mønster 5: Kombinere Context og Reducers (useReducer)

Kombinere Context med useReducer lar deg administrere kompleks tilstandslogikk og optimalisere ny renderinger. useReducer gir et forutsigbart tilstandshåndteringsmønster og lar deg oppdatere tilstanden basert på handlinger, noe som reduserer behovet for å sende flere setter-funksjoner gjennom Context.

Eksempel:


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

Nå kan komponenter få tilgang til tilstanden og sende handlinger ved hjelp av egendefinerte hooks. For eksempel:


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 (
    

Name: {user.name}

); }

Dette mønsteret fremmer en mer strukturert tilnærming til tilstandshåndtering og kan forenkle kompleks Context-logikk.

Fordeler:

Ulemper:

Mønster 6: Optimistiske oppdateringer

Optimistiske oppdateringer innebærer å oppdatere brukergrensesnittet umiddelbart som om en handling har lyktes, selv før serveren bekrefter det. Dette kan forbedre brukeropplevelsen betydelig, spesielt i situasjoner med høy latens. Det krever imidlertid nøye håndtering av potensielle feil.

Eksempel:

Tenk deg en applikasjon der brukere kan like innlegg. En optimistisk oppdatering vil umiddelbart øke antall likes når brukeren klikker på liker-knappen, og deretter tilbakestille endringen hvis serverforespørselen mislykkes.


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);
    // Optimistically update the like count
    dispatch({ type: 'INCREMENT_LIKES', payload: { postId } });

    try {
      // Simulate an API call
      await new Promise(resolve => setTimeout(resolve, 500));

      // If the API call is successful, do nothing (the UI is already updated)
    } catch (error) {
      // If the API call fails, revert the optimistic update
      dispatch({ type: 'DECREMENT_LIKES', payload: { postId } });
      alert('Failed to like post. Please try again.');
    } finally {
      setIsLiking(false);
    }
  };

  return (
    
  );
}

I dette eksemplet sendes `INCREMENT_LIKES`-handlingen umiddelbart, og deretter tilbakestilles den hvis API-kallet mislykkes. Dette gir en mer responsiv brukeropplevelse.

Fordeler:

Ulemper:

Velge riktig mønster

Det beste Context Provider-mønsteret avhenger av de spesifikke behovene til applikasjonen din. Her er et sammendrag for å hjelpe deg med å velge:

Ytterligere tips for å optimalisere Context-ytelsen

Konklusjon

React Context API er et kraftig verktøy, men det er viktig å bruke det riktig for å unngå ytelsesproblemer. Ved å forstå og bruke Context Provider-mønstrene som er diskutert i denne artikkelen, kan du effektivt administrere tilstanden, optimalisere ytelsen og bygge mer effektive og responsive React-applikasjoner. Husk å analysere dine spesifikke behov og velge det mønsteret som passer best til applikasjonens krav.

Ved å vurdere et globalt perspektiv, bør utviklere også sikre at tilstandshåndteringsløsninger fungerer sømløst på tvers av forskjellige tidssoner, valutaformater og regionale datakrav. For eksempel bør en datofunksjon i en Context lokaliseres basert på brukerens preferanse eller plassering, og sikre konsistente og nøyaktige datovisninger uavhengig av hvor brukeren får tilgang til applikasjonen fra.

React Context Provider-mønstre: Optimalisering av ytelse og unngåelse av problemer med ny rendering | MLOG