Lær, hvordan du optimerer React custom hooks ved at forstå og håndtere dependencies i useEffect. Forbedr ydeevnen og undgå almindelige faldgruber.
Dependencies i React Custom Hooks: Mestring af Effektoptimering for Bedre Ydeevne
React custom hooks er et kraftfuldt værktøj til at abstrahere og genbruge logik på tværs af dine komponenter. Men forkert håndtering af dependencies i `useEffect` kan føre til ydeevneproblemer, unødvendige re-renders og endda uendelige loops. Denne guide giver en omfattende forståelse af `useEffect` dependencies og best practices for at optimere dine custom hooks.
Forståelse af useEffect og Dependencies
`useEffect`-hook'en i React giver dig mulighed for at udføre sideeffekter i dine komponenter, såsom datahentning, DOM-manipulation eller opsætning af abonnementer. Det andet argument til `useEffect` er et valgfrit array af dependencies. Dette array fortæller React, hvornår effekten skal køre igen. Hvis nogen af værdierne i dependency-arrayet ændres mellem renders, vil effekten blive eksekveret igen. Hvis dependency-arrayet er tomt (`[]`), vil effekten kun køre én gang efter den indledende render. Hvis dependency-arrayet udelades helt, vil effekten køre efter hver render.
Hvorfor Dependencies er Vigtige
Dependencies er afgørende for at kontrollere, hvornår din effekt kører. Hvis du inkluderer en dependency, der faktisk ikke behøver at udløse effekten, vil du ende med unødvendige genkørsler, hvilket potentielt kan påvirke ydeevnen. Omvendt, hvis du udelader en dependency, der *skal* udløse effekten, opdateres din komponent måske ikke korrekt, hvilket fører til fejl og uventet adfærd. Lad os se på et grundlæggende eksempel:
import React, { useState, useEffect } from 'react';
function ExampleComponent({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUserData(data);
}
fetchData();
}, [userId]); // Dependency array: only re-run when userId changes
if (!userData) {
return Loading...
;
}
return (
{userData.name}
{userData.email}
);
}
export default ExampleComponent;
I dette eksempel henter effekten brugerdata fra et API. Dependency-arrayet inkluderer `userId`. Dette sikrer, at effekten kun kører, når `userId`-prop'en ændres. Hvis `userId` forbliver den samme, vil effekten ikke køre igen, hvilket forhindrer unødvendige API-kald.
Almindelige Faldgruber og Hvordan Man Undgår Dem
Flere almindelige faldgruber kan opstå, når man arbejder med `useEffect` dependencies. At forstå disse faldgruber og hvordan man undgår dem, er essentielt for at skrive effektiv og fejlfri React-kode.
1. Manglende Dependencies
Den mest almindelige fejl er at udelade en dependency, der *burde* være inkluderet i dependency-arrayet. Dette kan føre til "stale closures" og uventet adfærd. For eksempel:
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // Potential issue: `count` is not a dependency
}, 1000);
return () => clearInterval(intervalId);
}, []); // Empty dependency array: effect runs only once
return Count: {count}
;
}
export default Counter;
I dette eksempel er `count`-variablen ikke inkluderet i dependency-arrayet. Som et resultat bruger `setInterval`-callback'en altid den oprindelige værdi af `count` (som er 0). Tælleren vil ikke tælle korrekt op. Den korrekte version bør inkludere `count` i dependency-arrayet:
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(prevCount => prevCount + 1); // Correct: use functional update
}, 1000);
return () => clearInterval(intervalId);
}, []); // Now no dependency is needed since we use the functional update form.
return Count: {count}
;
}
export default Counter;
Lærdom: Sørg altid for, at alle variabler, der bruges inde i effekten og er defineret uden for effektens scope, er inkluderet i dependency-arrayet. Hvis muligt, brug funktionelle opdateringer (`setCount(prevCount => prevCount + 1)`) for at undgå behovet for `count`-dependency'en.
2. Inkludering af Unødvendige Dependencies
Inkludering af unødvendige dependencies kan føre til overdreven re-rendering og forringet ydeevne. Overvej for eksempel en komponent, der modtager en prop, som er et objekt:
import React, { useState, useEffect } from 'react';
function DisplayData({ data }) {
const [processedData, setProcessedData] = useState(null);
useEffect(() => {
// Perform some complex data processing
const result = processData(data);
setProcessedData(result);
}, [data]); // Problem: `data` is an object, so it changes on every render
function processData(data) {
// Complex data processing logic
return data;
}
if (!processedData) {
return Loading...
;
}
return {processedData.value}
;
}
export default DisplayData;
I dette tilfælde, selvom indholdet af `data`-objektet logisk set forbliver det samme, oprettes et nyt objekt ved hver render af den overordnede komponent. Det betyder, at `useEffect` vil køre igen ved hver render, selvom databehandlingen faktisk ikke behøver at blive udført igen. Her er et par strategier til at løse dette:
Løsning 1: Memoization med `useMemo`
Brug `useMemo` til at memoize `data`-prop'en. Dette vil kun genskabe `data`-objektet, hvis dets relevante egenskaber ændres.
import React, { useState, useEffect, useMemo } from 'react';
function ParentComponent() {
const [value, setValue] = useState(0);
// Memoize the `data` object
const data = useMemo(() => ({ value }), [value]);
return ;
}
function DisplayData({ data }) {
const [processedData, setProcessedData] = useState(null);
useEffect(() => {
// Perform some complex data processing
const result = processData(data);
setProcessedData(result);
}, [data]); // Now `data` only changes when `value` changes
function processData(data) {
// Complex data processing logic
return data;
}
if (!processedData) {
return Loading...
;
}
return {processedData.value}
;
}
export default ParentComponent;
Løsning 2: Destrukturering af Prop'en
Send individuelle egenskaber af `data`-objektet som props i stedet for hele objektet. Dette giver `useEffect` mulighed for kun at køre igen, når de specifikke egenskaber, det afhænger af, ændres.
import React, { useState, useEffect } from 'react';
function ParentComponent() {
const [value, setValue] = useState(0);
return ; // Pass `value` directly
}
function DisplayData({ value }) {
const [processedData, setProcessedData] = useState(null);
useEffect(() => {
// Perform some complex data processing
const result = processData(value);
setProcessedData(result);
}, [value]); // Only re-run when `value` changes
function processData(value) {
// Complex data processing logic
return { value }; // Wrap in object if needed inside DisplayData
}
if (!processedData) {
return Loading...
;
}
return {processedData.value}
;
}
export default ParentComponent;
Løsning 3: Brug af `useRef` til at Sammenligne Værdier
Hvis du har brug for at sammenligne *indholdet* af `data`-objektet og kun køre effekten igen, når indholdet ændres, kan du bruge `useRef` til at gemme den tidligere værdi af `data` og udføre en dyb sammenligning.
import React, { useState, useEffect, useRef } from 'react';
import { isEqual } from 'lodash'; // Requires lodash library (npm install lodash)
function DisplayData({ data }) {
const [processedData, setProcessedData] = useState(null);
const previousData = useRef(data);
useEffect(() => {
if (!isEqual(data, previousData.current)) {
// Perform some complex data processing
const result = processData(data);
setProcessedData(result);
previousData.current = data;
}
}, [data]); // `data` is still in the dependency array, but we check for deep equality
function processData(data) {
// Complex data processing logic
return data;
}
if (!processedData) {
return Loading...
;
}
return {processedData.value}
;
}
export default DisplayData;
Bemærk: Dybe sammenligninger kan være dyre, så brug denne tilgang med omhu. Dette eksempel afhænger også af `lodash`-biblioteket. Du kan installere det ved hjælp af `npm install lodash` eller `yarn add lodash`.
Lærdom: Overvej omhyggeligt, hvilke dependencies der virkelig er nødvendige. Undgå at inkludere objekter eller arrays, der genoprettes ved hver render, hvis deres indhold logisk set forbliver det samme. Brug memoization, destrukturering eller dybe sammenligningsteknikker for at optimere ydeevnen.
3. Uendelige Loops
Forkert håndtering af dependencies kan føre til uendelige loops, hvor `useEffect`-hook'en kontinuerligt kører igen, hvilket får din komponent til at fryse eller gå ned. Dette sker ofte, når effekten opdaterer en state-variabel, der også er en dependency for effekten. For eksempel:
import React, { useState, useEffect } from 'react';
function InfiniteLoop() {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data from an API
fetch('https://api.example.com/data')
.then(response => response.json())
.then(result => {
setData(result); // Updates `data` state
});
}, [data]); // Problem: `data` is a dependency, so the effect re-runs when `data` changes
if (!data) {
return Loading...
;
}
return {data.value}
;
}
export default InfiniteLoop;
I dette eksempel henter effekten data og sætter det i `data`-state-variablen. Men `data` er også en dependency for effekten. Dette betyder, at hver gang `data` opdateres, kører effekten igen, henter data igen og sætter `data` igen, hvilket fører til et uendeligt loop. Der er flere måder at løse dette på:
Løsning 1: Tomt Dependency-array (Kun ved Indledende Indlæsning)
Hvis du kun vil hente dataene én gang, når komponenten mounter, kan du bruge et tomt dependency-array:
import React, { useState, useEffect } from 'react';
function InfiniteLoop() {
const [data, setData] = useState(null);
useEffect(() => {
// Fetch data from an API
fetch('https://api.example.com/data')
.then(response => response.json())
.then(result => {
setData(result);
});
}, []); // Empty dependency array: effect runs only once
if (!data) {
return Loading...
;
}
return {data.value}
;
}
export default InfiniteLoop;
Løsning 2: Brug en Separat State til Indlæsning
Brug en separat state-variabel til at spore, om dataene er blevet indlæst. Dette forhindrer effekten i at køre igen, når `data`-state'en ændres.
import React, { useState, useEffect } from 'react';
function InfiniteLoop() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (isLoading) {
// Fetch data from an API
fetch('https://api.example.com/data')
.then(response => response.json())
.then(result => {
setData(result);
setIsLoading(false);
});
}
}, [isLoading]); // Only re-run when `isLoading` changes
if (!data) {
return Loading...
;
}
return {data.value}
;
}
export default InfiniteLoop;
Løsning 3: Betinget Datahentning
Hent kun dataene, hvis de i øjeblikket er null. Dette forhindrer efterfølgende hentninger, efter at de indledende data er blevet indlæst.
import React, { useState, useEffect } from 'react';
function InfiniteLoop() {
const [data, setData] = useState(null);
useEffect(() => {
if (!data) {
// Fetch data from an API
fetch('https://api.example.com/data')
.then(response => response.json())
.then(result => {
setData(result);
});
}
}, [data]); // `data` is still a dependency but the effect is conditional
if (!data) {
return Loading...
;
}
return {data.value}
;
}
export default InfiniteLoop;
Lærdom: Vær yderst forsigtig, når du opdaterer en state-variabel, der også er en dependency for effekten. Brug tomme dependency-arrays, separate loading-states eller betinget logik for at forhindre uendelige loops.
4. Mutable Objekter og Arrays
Når man arbejder med mutable objekter eller arrays som dependencies, vil ændringer i objektets egenskaber eller arrayets elementer ikke automatisk udløse effekten. Dette skyldes, at React udfører en overfladisk sammenligning (shallow comparison) af dependencies.
import React, { useState, useEffect } from 'react';
function MutableObject() {
const [config, setConfig] = useState({ theme: 'light', language: 'en' });
useEffect(() => {
console.log('Config changed:', config);
}, [config]); // Problem: Changes to `config.theme` or `config.language` won't trigger the effect
const toggleTheme = () => {
// Mutating the object
config.theme = config.theme === 'light' ? 'dark' : 'light';
setConfig(config); // This won't trigger a re-render or the effect
};
return (
Theme: {config.theme}, Language: {config.language}
);
}
export default MutableObject;
I dette eksempel ændrer `toggleTheme`-funktionen direkte på `config`-objektet, hvilket er dårlig praksis. Reacts overfladiske sammenligning ser, at `config` stadig er det *samme* objekt i hukommelsen, selvom dets egenskaber er ændret. For at rette dette skal du oprette et *nyt* objekt, når du opdaterer state:
import React, { useState, useEffect } from 'react';
function MutableObject() {
const [config, setConfig] = useState({ theme: 'light', language: 'en' });
useEffect(() => {
console.log('Config changed:', config);
}, [config]); // Now the effect will trigger when `config` changes
const toggleTheme = () => {
setConfig({ ...config, theme: config.theme === 'light' ? 'dark' : 'light' }); // Create a new object
};
return (
Theme: {config.theme}, Language: {config.language}
);
}
export default MutableObject;
Ved at bruge spread-operatoren (`...config`) opretter vi et nyt objekt med den opdaterede `theme`-egenskab. Dette udløser en re-render, og effekten bliver genkørt.
Lærdom: Behandl altid state-variabler som immutable. Når du opdaterer objekter eller arrays, skal du oprette nye instanser i stedet for at modificere eksisterende. Brug spread-operatoren (`...`), `Array.map()`, `Array.filter()` eller lignende teknikker til at oprette nye kopier.
Optimering af Custom Hooks med Dependencies
Nu hvor vi forstår de almindelige faldgruber, lad os se på, hvordan man optimerer custom hooks ved omhyggeligt at håndtere dependencies.
1. Memoization af Funktioner med `useCallback`
Hvis din custom hook returnerer en funktion, der bruges som en dependency i en anden `useEffect`, bør du memoize funktionen ved hjælp af `useCallback`. Dette forhindrer, at funktionen genoprettes ved hver render, hvilket unødigt ville udløse effekten.
import React, { useState, useEffect, useCallback } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async () => {
setIsLoading(true);
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
}, [url]); // Memoize `fetchData` based on `url`
useEffect(() => {
fetchData();
}, [fetchData]); // Now `fetchData` only changes when `url` changes
return { data, isLoading, error };
}
function MyComponent() {
const [userId, setUserId] = useState(1);
const { data, isLoading, error } = useFetchData(`https://api.example.com/users/${userId}`);
return (
{/* ... */}
);
}
export default MyComponent;
I dette eksempel er `fetchData`-funktionen memoized ved hjælp af `useCallback`. Dependency-arrayet inkluderer `url`, som er den eneste variabel, der påvirker funktionens adfærd. Dette sikrer, at `fetchData` kun ændres, når `url` ændres. Derfor vil `useEffect`-hook'en i `useFetchData` kun køre igen, når `url` ændres.
2. Brug af `useRef` til Stabile Referencer
Nogle gange har du brug for at tilgå den seneste værdi af en prop eller state-variabel inde i en effekt, men du ønsker ikke, at effekten skal køre igen, når den værdi ændres. I dette tilfælde kan du bruge `useRef` til at oprette en stabil reference til værdien.
import React, { useState, useEffect, useRef } from 'react';
function LogLatestValue({ value }) {
const latestValue = useRef(value);
useEffect(() => {
latestValue.current = value; // Update the ref on every render
}, [value]); // Update the ref when `value` changes
useEffect(() => {
// Log the latest value after 5 seconds
const timerId = setTimeout(() => {
console.log('Latest value:', latestValue.current); // Access the latest value from the ref
}, 5000);
return () => clearTimeout(timerId);
}, []); // Effect runs only once on mount
return Value: {value}
;
}
export default LogLatestValue;
I dette eksempel opdateres `latestValue`-ref'en ved hver render med den aktuelle værdi af `value`-prop'en. Men effekten, der logger værdien, kører kun én gang ved mount takket være det tomme dependency-array. Inde i effekten tilgår vi den seneste værdi ved hjælp af `latestValue.current`. Dette giver os mulighed for at tilgå den mest opdaterede værdi af `value` uden at få effekten til at køre igen, hver gang `value` ændres.
3. Oprettelse af Custom Abstraktion
Opret en custom comparator eller abstraktion, hvis du arbejder med et objekt, og kun en lille delmængde af dets egenskaber er vigtige for `useEffect`-kaldene.
import React, { useState, useEffect } from 'react';
// Custom comparator to only track theme changes.
function useTheme(config) {
const [theme, setTheme] = useState(config.theme);
useEffect(() => {
setTheme(config.theme);
}, [config.theme]);
return theme;
}
function ConfigComponent({ config }) {
const theme = useTheme(config);
return (
The current theme is {theme}
)
}
export default ConfigComponent;
Lærdom: Brug `useCallback` til at memoize funktioner, der bruges som dependencies. Brug `useRef` til at oprette stabile referencer til værdier, du har brug for at tilgå inde i effekter uden at få effekterne til at køre igen. Når du arbejder med komplekse objekter eller arrays, overvej at oprette custom comparators eller abstraktionslag for kun at udløse effekter, når relevante egenskaber ændres.
Globale Overvejelser
Når man udvikler React-applikationer til et globalt publikum, er det vigtigt at overveje, hvordan dependencies kan påvirke lokalisering og internationalisering. Her er nogle centrale overvejelser:
1. Locale-ændringer
Hvis din komponent afhænger af brugerens locale (f.eks. til formatering af datoer, tal eller valutaer), bør du inkludere locale i dependency-arrayet. Dette sikrer, at effekten kører igen, når locale ændres, og opdaterer komponenten med den korrekte formatering.
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns'; // Requires date-fns library (npm install date-fns)
function LocalizedDate({ date, locale }) {
const [formattedDate, setFormattedDate] = useState('');
useEffect(() => {
setFormattedDate(format(date, 'PPPP', { locale }));
}, [date, locale]); // Re-run when `date` or `locale` changes
return {formattedDate}
;
}
export default LocalizedDate;
I dette eksempel bruges `format`-funktionen fra `date-fns`-biblioteket til at formatere datoen i henhold til den specificerede locale. `locale` er inkluderet i dependency-arrayet, så effekten kører igen, når locale ændres, og opdaterer den formaterede dato.
2. Tidszoneovervejelser
Når du arbejder med datoer og tider, skal du være opmærksom på tidszoner. Hvis din komponent viser datoer eller tider i brugerens lokale tidszone, kan du have brug for at inkludere tidszonen i dependency-arrayet. Tidszoneændringer er dog mindre hyppige end locale-ændringer, så du kan overveje at bruge en separat mekanisme til at opdatere tidszonen, såsom en global context.
3. Valutaformatering
Når du formaterer valutaer, skal du bruge den korrekte valutakode og locale. Inkluder begge i dependency-arrayet for at sikre, at valutaen formateres korrekt for brugerens region.
import React, { useState, useEffect } from 'react';
function LocalizedCurrency({ amount, currency, locale }) {
const [formattedCurrency, setFormattedCurrency] = useState('');
useEffect(() => {
setFormattedCurrency(new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount));
}, [amount, currency, locale]); // Re-run when `amount`, `currency`, or `locale` changes
return {formattedCurrency}
;
}
export default LocalizedCurrency;
Lærdom: Når du udvikler til et globalt publikum, skal du altid overveje, hvordan dependencies kan påvirke lokalisering og internationalisering. Inkluder locale, tidszone og valutakode i dependency-arrayet, når det er nødvendigt, for at sikre, at dine komponenter viser data korrekt for brugere i forskellige regioner.
Konklusion
At mestre `useEffect`-dependencies er afgørende for at skrive effektive, fejlfri og performante React custom hooks. Ved at forstå de almindelige faldgruber og anvende de optimeringsteknikker, der er diskuteret i denne guide, kan du skabe custom hooks, der er både genanvendelige og vedligeholdelsesvenlige. Husk at overveje omhyggeligt, hvilke dependencies der virkelig er nødvendige, brug memoization og stabile referencer, hvor det er relevant, og vær opmærksom på globale overvejelser som lokalisering og internationalisering. Ved at følge disse best practices kan du frigøre det fulde potentiale af React custom hooks og bygge applikationer af høj kvalitet til et globalt publikum.
Denne omfattende guide har dækket meget. For at opsummere, her er de vigtigste takeaways:
- Forstå formålet med dependencies: De styrer, hvornår din effekt kører.
- Undgå manglende dependencies: Sørg for, at alle variabler, der bruges inde i effekten, er inkluderet.
- Fjern unødvendige dependencies: Brug memoization, destrukturering eller dyb sammenligning.
- Forebyg uendelige loops: Vær forsigtig, når du opdaterer state-variabler, der også er dependencies.
- Behandl state som immutable: Opret nye objekter eller arrays ved opdatering.
- Memoize funktioner med `useCallback`: Forhindr unødvendige re-renders.
- Brug `useRef` til stabile referencer: Tilgå den seneste værdi uden at udløse re-renders.
- Overvej globale implikationer: Tag højde for ændringer i locale, tidszone og valuta.
Ved at anvende disse principper kan du skrive mere robuste og effektive React custom hooks, der vil forbedre ydeevnen og vedligeholdelsen af dine applikationer.