Arvoihin viittaaminen Refillä

Kun haluat komponentin “muistavan” jotain tietoa, mutta et halua tiedon triggeröivän uudelleenrenderöintiä, voit käyttää refiä.

Tulet oppimaan

  • Miten lisätä ref komponenttiisi
  • Miten päivittää refin arvo
  • Miten refit eroavat tilasta
  • Miten käyttää refiä turvallisesti

Refin lisääminen komponenttiisi

Voit lisätä refin komponenttiisi importaamalla useRef Hookin Reactista:

import { useRef } from 'react';

Komponenttisi sisällä kutsu useRef hookkia ja välitä oletusarvo, jota haluat viitata ainoana argumenttina. Esimerkiksi, tässä on ref arvolla 0:

const ref = useRef(0);

useRef palauttaa seuraavanlaisen olion:

{
current: 0 // Arvo, jonka välitit useRef funktiolle
}
Nuoli jossa on 'current' kirjoitettuna, joka on sijoitettu taskuun jossa on 'ref' kirjoitettuna.

Illustrated by Rachel Lee Nabors

Pääset käsiksi nykyiseen refin arvoon ref.current ominaisuuden kautta. Tämä arvo on tarkoituksella muokattavissa, eli voit sekä lukea että kirjoittaa siihen. Se on kuin salainen tasku komponentissasi, jota React ei seuraa. (Tämä on se mikä tekee refistä “pelastusluukun” Reactin yksisuuntaisesta datavirtauksesta—josta alla lisää!)

Täss, painike kasvattaa ref.current arvoa joka kerta kun sitä painetaan:

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

Ref osoittaa numeroon, mutta, kuten tila, voit viitata mihin tahansa: merkkijonoon, olioon tai jopa funktioon. Tilaan verrattuna, ref on tavallinen JavaScript-olio, jolla on current-ominaisuus, jota voit lukea ja muokata.

Huomaa, että komponentti ei renderöidy uudelleen joka kerta kun arvo kasvaa. Kuten tila, refit säilyvät Reactin uudelleenrenderöintien välillä. Kuitenkin, tilan asettaminen uudelleenrenderöi komponentin. Refin päivittäminen ei!

Esimerkki: sekuntikellon rakentaminen

Voit yhdistää refin ja tilan samaan komponenttiin. Esimerkiksi, tehdään sekuntikello, jonka käyttäjä voi käynnistää tai pysäyttää nappia painamalla. Jotta voidaan näyttää kuinka paljon aikaa on kulunut siitä kun käyttäjä on painanut “Start” nappia, sinun täytyy pitää kirjaa siitä milloin käyttäjä painoi “Start” nappia ja mitä nykyinen aika on. Tätä tietoa käytetään renderöinnissä, joten pidä se tilassa:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

Kun käyttäjä painaa “Start”, käytät setInterval funktiota päivittääksesi ajan joka 10 millisekuntin välien:

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Aloita laskeminen.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Päivitä tämänhetkinen aika joka 10ms välein.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

Kun “Stop” nappia painetaan, sinun täytyy peruuttaa olemassa oleva ajastin, jotta se lopettaa now tilamuuttujan päivittämisen. Voit tehdä tämän kutsumalla clearInterval funktiota, mutta sinun täytyy antaa sille ajastimen ID, joka palautettiin aiemmin setInterval kutsun kautta kun käyttäjä painoi “Start”. Sinun täytyy pitää ajastimen ID jossain. Koska ajastimen ID:tä ei käytetä renderöinnissä, voit pitää sen refissä:

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

Kun tietoa käytetään renderöinnissä, pidä se tilassa. Kun tietoa tarvitaan vain tapahtumankäsittelijöissä ja sen muuttaminen ei vaadi uudelleenrenderöintiä, refin käyttäminen voi olla tehokkaampaa.

Refin ja tilan erot

Ehkäpä ajattelet, että refit vaikuttavat vähemmän “tiukilta” kuin tila—voit muokata niitä tilan asettamisfunktion käyttämisen sijaan. Mutta useimmissa tapauksissa haluat käyttää tilaa. Refit ovat “pelastusluukku”, jota et tarvitse usein. Tässä on miten tila ja refit vastaavat toisiaan:

reftila
useRef(initialValue) palauttaa { current: initialValue }useState(initialValue) palauttaa tilamuuttujan nykyisen arvon ja tilan asetusfunktion ( [value, setValue])
Ei triggeröi uudelleenrenderöintiä kun muutat sitä.Triggeröi uudelleenrenderöinnin kun muutat sitä.
Mutatoitavissa—voit muokata ja päivittää current:n arvoa renderöintiprosessin ulkopuolella.Ei-mutatoitavissa—sinun täytyy käyttää tilan asetusfunktiota muokataksesi tilamuuttujaa jonottaaksesi uudelleenrenderöinti.
Sinuun ei tulisi lukea (tai kirjoittaa) current arvoa kesken renderöinnin.Voit lukea tilaa koska tahansa. Kuitenkin, jokaisella renderöinnillä on oma tilakuvansa tilasta, joka ei muutu.

Tässä on laskuri-painike, joka on toteutettu tilalla:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>
  );
}

Koska count arvo näytetään, on järkevää käyttää tilaa arvon tallentamiseen. Kun laskurin arvo asetetaan setCount() funktiolla, React renderöi komponentin uudelleen ja ruutu päivittyy vastaamaan uutta arvoa.

Jos yrität toteuttaa tämän refillä, React ei koskaan renderöi komponenttia uudelleen, joten et koskaan näe laskurin arvon muuttuvan! Katso miten tämän painikkeen klikkaaminen ei päivitä sen tekstiä:

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // Tämä ei uudeleenrenderöi komponenttia!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      You clicked {countRef.current} times
    </button>
  );
}

Tämä on syy miksi refin current arvon lukeminen renderöinnin aikana johtaa epäluotettavaan koodiin. Jos tarvitset tätä, käytä tilaa sen sijaan.

Syväsukellus

Miten useRef toimii?

Vaikka sekä useState että useRef on tarjottu Reactin puolesta, periaatteessa useRef voitaisiin toteuttaa useState:n päälle. Voit kuvitella, että Reactin sisällä useRef on toteutettu näin:

// Reactin sisällä
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}

Ensimmäisen renderöinnin aikana, useRef palauttaa { current: initialValue }. Tämä olio tallennetaan Reactin puolelle, joten seuraavalla renderöinnillä sama olio palautetaan. Huomaa, että tilan asetusfunktiota ei käytetä tässä esimerkissä. Se on tarpeeton, koska useRef:n tarvitsee aina palauttaa sama olio!

React tarjoaa sisäänrakennetun version useRef:sta koska se on tarpeeksi yleinen. Mutta voit ajatella sitä tavallisena tilamuuttujana ilman asetusfunktiota. Jos olet tutustunut olio-ohjelmointiin, refit voivat muistuttaa sinua instanssimuuttujista, mutta this.something sijaan kirjoitat somethingRef.current.

Milloin käyttää refiä

Tyypillisesti, käytät refiä kun komponenttisi täytyy “astua Reactin ulkopuolelle” ja kommunikoida ulkoisten rajapintojen kanssa—usein selaimen API:n, joka ei vaikuta komponentin ulkonäköön. Tässä on muutamia näitä harvinaisia tilanteita:

Jos komponenttisi tarvitsee tallentaa arvoa, mutta se ei vaikuta renderöinnin logiikkaan, valitse ref.

Parhaat käytännöt refille

Näitä periaatteita noudattaen komponenteistasi tulee ennakoitavampia:

  • Käsittele refejä kuten pelastusluukkua. Refit ovat hyödyllisiä kun työskentelet ulkoisten järjestelmien tai selaimen API:n kanssa. Jos suuri osa sovelluksesi logiikasta ja datavirtauksesta riippuu refeistä, saatat haluta miettiä lähestymistapaasi uudelleen.
  • Älä lue tai kirjoita ref.current:iin kesken renderöinnin. Jos jotain tietoa tarvitaan kesken renderöinnin, käytä tilaa sen sijaan. Koska React ei tiedä milloin ref.current muuttuu, jopa sen lukeminen renderöinnin aikana tekee komponenttisi käyttäytymisestä vaikeasti ennakoitavaa. (Ainoa poikkeus tähän on koodi kuten if(!ref.current) ref.current = new Thing() joka asettaa refin vain kerran ensimäisellä renderöinnillä.)

Reactin tilan rajoitukset eivät päde refiin. Esimerkiksi tila toimii tilannekuvana jokaiselle renderöinnille ja se ei päivity synkronisesti. Mutta kun muokkaat refin nykyistä arvoa, se muuttuu välittömästi:

ref.current = 5;
console.log(ref.current); // 5

Tämä johtuu siitä, että ref on tavallinen JavaScript olio, joten se käyttäytyy samoin.

Sinun ei myöskään tarvitse huolehtia mutaatioiden välttämistä, kun työskentelet refin kanssa. Jos olio, jota muutat ei ole käytössä renderöinnissä, React ei välitä mitä teet refin tai sen sisällön kanssa.

Ref ja DOM

Voit osoittaa refin mihin tahansa arvoon. Kuitenkin yleisin käyttökohde refille on DOM elementin käsittely. Esimerkiksi, tämä on kätevää jos haluat focusoida syöttölaatikon ohjelmakoodissa. Kun annat refin ref-attribuuttiin JSX:ssä, kuten <div ref={myRef}>, React asettaa vastaavan DOM elementin myRef.current:iin. Voit lukea lisää tästä Manipulating the DOM with Refs.

Kertaus

  • Refit ovat pelastusluukku arvojen pitämiseen, jotka eivät ole käytössä renderöinnissä. Et tarvitse niitä usein.
  • Ref on perus JavaScript-olio, jolla on yksi ominaisuus nimeltään current, jonka voit lukea tai asettaa.
  • Voit pyytää Reactia antamaan sinulle refin kutsumalla useRef Hookia.
  • Kuten tila, refit antavat sinun säilyttää tietoa komponentin uudelleenrenderöinnin välillä.
  • Toisin kuin tila, refin current-arvon asettaminen ei aiheuta uudelleenrenderöintiä.
  • Älä lue tai kirjota ref.current-arvoa renderöinnin aikana. Tämä tekee komponentistasi vaikeasti ennustettavan.

Haaste 1 / 4:
Korjaa rikkinäinen chat-kenttä

Kirjoita viesti ja paina “Send” painiketta. Huomaat, että viesti ilmestyy kolmen sekunnin viiveellä. Tämän ajan aikana näet “Undo” painikkeen. Paina sitä. Tämä “Undo” painike on tarkoitettu pysäyttämään “Sent!” viesti näkyvistä. Se tekee tämän kutsumalla clearTimeout funktiota timeout ID:llä, joka tallennettiin handleSend funktion aikana. Kuitenkin, vaikka “Undo” painiketta painettaisiin, “Sent!” viesti ilmestyy silti. Etsi miksi se ei toimi, ja korjaa se.

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  let timeoutID = null;

  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      alert('Sent!');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Sending...' : 'Send'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Undo
        </button>
      }
    </>
  );
}