React-hook: Slik husker du på forrige verdi i React

Kaptein Krok: Hvordan skal vi få tak i forrige utgave av applikasjons-tilstanden med useEffect?

📸: Privat / Harrison Broadbent / Unsplash
📸: Privat / Harrison Broadbent / Unsplash Vis mer

iI gamle dager, for sånn… 3 år siden, skrev vi React-koden vår ganske annerledes enn vi gjør nå. På den tiden brukte vi såkalte klassekomponenter, med “livssyklusmetoder” for å reagere på endringer i props og state.

Koden din kunne for eksempel se slik ut:

class Eksempel extends React.Component {
  componentDidUpdate(prevProps, nextProps) {
    if (prevProps.id !== nextProps.id) {
      analytics.log(
        `ID endret seg fra ${prevProps.id} til ${nextProps.id}`
      );
    }
  }
  render() {
    return <h1>Hei hei</h1>;
  }
}

Nå bruker man den litt mer hendige useEffect hooken til å oppnå det samme. Fordelene er mange, men en av ulempene er at man ikke lenger mottar forrige versjon av propsene — og da blir plutselig dette vanskelig, eller?

const Eksempel = (props) => {
  useEffect(() => {
    analytics.log(
      `ID endret seg fra ${???} til ${nextProps.id}`
    );
  }, [props.id]);
  return <h1>Hei hei</h1>;
}

Hvor skal vi få prevProps fra? Blir man tvunget til å bruke klassekomponenter igjen?!? 👀

useState vs useRef

Før vi titter på hvordan vi løser dette med moderne React-APIer, tenkte jeg det var greit å gi en liten innføring i forskjellen mellom to innebygde React-hooks — nemlig useState og den noe mer eksotiske useRef.

Om du har skrevet React i det siste, har du sikkert vært borti useState . Du sender inn en initiell verdi, og returnerer et tuple med verdien, og en funksjon for å oppdatere verdien.

const [id, setId] = React.useState('min-id');
console.log(id);  // 'min-id'

Hvorfor trenger vi det egentlig? Jo, det er for at React skal kunne legge merke til at verdien endret seg, og så “rendre” — eller kalle — komponenten din igjen for å oppdatere seg igjen. Om state-settere kunne prate, ville nok setId sagt noe slikt:

«“Hei React, nå har `id` fått ny verdi! Kall komponenten min på nytt, så du ser hvordan ting burde endre seg!”»

Det finnes en annen måte å holde på verdier på tvers av komponent-kall (renders) også, og det er med useRef . Som i useState sender du inn en initiell verdi, men her får du istedenfor tilbake et objekt med nøkkelen current og den initielle verdien din som verdi.

const idRef = useRef('min-id')
console.log(idRef.current); // 'min-id'

Bortsett fra at strukturen er litt annerledes, er det en viktig forskjell mellom de to — når du endrer verdien til idRef , sier du ikke ifra til React! Derfor rendrer ikke React komponenten din på nytt, og får egentlig ikke med seg endringen.

Dette kan være veldig nyttig av og til — blant annet når du vil huske på forrige verdi av noe.

usePreviousValue

La oss lage en custom krok for å huske på forrige verdi. Den kommer til å se slik ut:

const usePreviousValue = (value) => {
  const previousRef = useRef();
  useEffect(() => {
    previousRef.current = value;
  }, [value]);
  return previousValue.current
}

Det ser kanskje litt merkelig ut? Ikke fortvil —jeg skal forklare.

Vi starter med å lage oss en ny ref med useRef — dette er den som skal holde på den “forrige” verdien vår. Vi vil ikke at React skal re-rendre bare fordi den forrige verdien endret seg — den vil jo alltid endre seg i takt med verdien den følger.

Neste steg er å oppdatere den “forrige verdien” hver gang den “neste verdien” oppdaterer seg. Det bruker vi en useEffect til å oppnå. Vi sender inn value i avhengighetslista, slik at sideeffekten kjører hver gang value endres, og “forrige verdi” oppdateres til å være den “nye forrige verdien”.

Helt til slutt returnerer vi den forrige verdien, slik at vi kan bruke den i koden vår!

const Eksempel = (props) => {
  const previousId = usePreviousValue(props.id);
  useEffect(() => {
    analytics.log(
      `ID endret seg fra ${previousId} til ${nextProps.id}`
    );
  }, [props.id]);
  return <h1>Hei hei</h1>;
}

Eller om du vil huske på alle props:

const Eksempel = (props) => {
  const prevProps = usePreviousValue(props);
  useEffect(() => {
    analytics.log(
      `ID endret seg fra ${prevProps.id} til ${nextProps.id}`
    );
  }, [props.id]);
  return <h1>Hei hei</h1>;
}

Nyttig triks å kunne

Dette kan være en nyttig teknikk å ha i ermet. Som et eksempel kan vi jo forbedre usePersistedState hooken vi laget i denne artikkelen? La oss fjerne ting fra local storage om keyen endrer seg:

const usePersistedState = (key, initialState) => {
  const [state, setState] = React.useState(
    localStorage.getItem(key) !== undefined 
    ? JSON.parse(localStorage.getItem(key))
    : initialState
  );
  const previousKey = usePreviousValue(key);
  
  React.useEffect(() => {
    const item = localStorage.getItem(key);
    if (item !== undefined) {
      setState(JSON.parse(item));
    } 
  }, [key]);
  React.useEffect(() => {
    if (previousKey !== key) {
      localStorage.deleteItem(key);
    }
    localStorage.setItem(key, JSON.stringify(state));
  }, [state, key]);
  return [state, setState];
};

Og vipps, så har vi ryddet opp etter oss også.

Har du en krok-idé?

Jeg har en rekke enkle hooks som er greie å ha i verktøykassa. Om du har en du er spesielt fornøyd med, så send meg en mail — så kan det godt hende den dukker opp i denne serien!