📾: Privat / Unsplash

React-hook: Fiks en minnelekkasje i React 🚿

Kaptein Krok: LĂŠr Ă„ takle feilen "Can’t perform a React state update on an unmounted component".

Publisert

Kaptein Krok ☠

Kaptein Krok-serien blir fÞrst publisert pÄ Bekk sin blogg. FÞlg bloggen deres pÄ Medium.

iHar du noensinne prÞvd Ä sette state asynkront i React, men fÄtt fÞlgende strofe rett i trynet?

Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

Det har jeg ogsĂ„. Det skjer typisk nĂ„r du har trigget litt henting av data, og navigert videre fĂžr du fikk svar. Og det kan – om du ikke er forsiktig – fĂžre til en minnelekkasje, som kan gjĂžre appen din tregere og tregere etter hvert som den kjĂžrer.

Minnelekkasje? I MIN APP?!?

Her er et eksempel pÄ kode som kan fÞre til en slik situasjon:

Om Eksempel ikke lenger er “mounted” (rendret pĂ„ skjermen) nĂ„r setData blir kalt, vil du fĂ„ en sinna melding i consolen din.

const Eksempel = () => {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(newData => setData(newData);
    }, []);
  return <DataDisplayer2000 data={data} />;
}

Fiks minnelekkasjen min đŸ˜±

For Ă„ fikse dette, mĂ„ vi sjekke om komponenten er mounted, og spore dette pĂ„ et vis. Det kan vi gjĂžre ved Ă„ bruke to innebygde hooks – useRef og useEffect:

const Eksempel = () => {
  const [data, setData] = useState(null);
  const isMounted = useRef(true);
  useEffect(() => {
    return () => {
      isMounted.current = false;
    }
  }, []);
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(newData => {
        if (isMounted.current) {
          setData(newData);
        }
      });
    }, []);
  return <DataDisplayer2000 data={data} />;
}

Her ble det fort mye kode som ikke hadde sĂ„ mye med den originale koden vĂ„r Ă„ gjĂžre, sĂ„ her kan vi refaktorere litt. FĂžrst – la oss lage en custom hook som forteller oss om komponenten er mounted eller ei.

const useIsMounted = () => {
  const isMountedRef = useRef(true);
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    }
  }, []);
  return isMountedRef.current
};

Her har vi flyttet all logikken vÄr inn i useIsMounted, som rett og slett returnerer en boolean som indikerer det vi lurte pÄ. La oss ta den i bruk i eksemplet vÄrt!


const Eksempel = () => {
  const [data, setData] = useState(null);
  const isMounted = useRef(true);
  useEffect(() => {
    return () => {
      isMounted.current = false;
    }
  }, []);
  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(newData => {
        if (isMounted.current) {
          setData(newData);
        }
      });
    }, []);
  return <DataDisplayer2000 data={data} />;
}

Ah, straks bedre! Eller
?

En bug 🐛

Her har vi faktisk innfĂžrt en ganske lei bug — isMounted vil alltid vĂŠre true! Det er fordi hooken vĂ„r aldri blir kalt pĂ„ ny, som gjĂžr at isMounted som brukes inni useEffect vil vĂŠre true, selv om ref-en vĂ„r er false.

Dette kan vi heldigvis rydde opp i med Ă„ returnere en funksjon som returnerer ref-verdien istedenfor verdien direkte. Med andre ord — la oss endre hooken vĂ„r til dette:

const useIsMounted = () => {
  const isMountedRef = useRef(true);
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    }
  }, []);
  // se her - nÄ returnerer vi en funksjon!
  return () => isMountedRef.current
};

Bruken mĂ„ ogsĂ„ oppdateres — nĂ„ mĂ„ vi kalle isMounted for Ă„ fĂ„ den nyeste verdien:

const Eksempel = () => {
  const [data, setData] = useState(null);
  const isMounted = useIsMounted();
useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(newData => {
        if (isMounted()) {
          setData(newData);
        }
      });
    }, []);
  return <DataDisplayer2000 data={data} />;
}

Og med dette, sĂ„ fungerer det i alle fall slik det skal. Tusen takk til Erlend Åmdal som pekte ut denne litt flaue feilen for meg.

Men kanskje vi kan gjĂžre ting enda enklere? Kanskje man rett og slett kan bygge denne funksjonaliteten inn i useState , og returnere isMounted som en tredje parameter? đŸ€”

Hils pĂ„ useMountedState 🐮

La oss prÞve oss pÄ en liten implementasjon:

const useMountedState = (defaultValue) => {
  const isMounted = useIsMounted();
  const [state, setState] = useState(defaultValue);
  const defensiveSetState = useCallback((newValue) => {
    if (isMounted()) {
      setState(newValue);
    }
  }}, [])
  return [state, defensiveSetState, isMounted];
};

useMountedState er egentlig bare en veldig tynn wrapper av den innebygde useState hooken – hvor man unngĂ„r Ă„ kalle setState om man ikke er mounted.

Dette vil fjerne advarselen, men det kan fortsatt vĂŠre situasjoner der man vil gjĂžre nye kall, dataprosessering osv. etter det fĂžrste kallet – og da vil man jo fortsatt ha denne minnelekkasjen. Heldigvis kan vi nĂ„ skrive kode for Ă„ unngĂ„ Ă„ fortsette om den tredje verdien som returneres – isMounted – er false.

La oss ta en siste refaktorering av eksemplet vÄrt:

const Eksempel = () => {
  const [data, setData, isMounted] = useMountedState(null);
  useEffect(() => {
    fetch('/api/data')
      .then(res => isMounted() && res.json())
      .then(newData => {
        setData(newData);
      });
    }, []);
  return <DataDisplayer2000 data={data} />;
}

NĂ„ ser koden like enkel ut som den gjorde i utgangspunktet – men helt minnelekkasje-fri. Vi hopper til og med over JSON-parsingen om ikke komponenten er pĂ„ skjermen lenger!

Merk at vi har utelatt isMounted fra useEffect sin dependency array. Den vil ikke trigge en re-render, siden isMounted er en useRef – verdi, sĂ„nn egentlig.

Har du en krok-idé?

Vi har en rekke enkle hooks som er greie Ă„ ha i verktĂžykassa. Om du har en du er spesielt fornĂžyd med, sĂ„ send oss en mail – sĂ„ kan det godt hende den dukker opp i denne serien!

Powered by Labrador CMS