DOM:in manipulointi Refillä
React automaattisesti päivittää DOM:in vastaamaan renderöinnin lopputulosta, joten sitä ei usein tarvitse manipuloida. Kuitenkin joskus saatat tarvita pääsyn Reactin hallinnoimiin DOM elementteihin—esimerkiksi, kohdentaaksesi elementtiin, scrollata siihen, tai mitata sen kokoa ja sijaintia. Reactissa ei ole sisäänrakennettua tapaa tehdä näitä asioita, joten tarvitset viittauksen eli refin DOM noodiin.
Tulet oppimaan
- Miten päästä käsiksi Reactin hallinnoimaan DOM noodiin
ref
attribuutilla - Miten
ref
attribuutti liittyyuseRef
Hookkiin - Miten päästä käsiksi toisen komponentin DOM noodiin
- Missä tapauksissa on turvallista muokata Reactin hallinnoimaa DOM:ia
Refin saaminen noodille
Päästäksesi käsiksi Reactin hallinnoimaan DOM noodiin, ensiksi, tuo useRef
Hookki:
import { useRef } from 'react';
Sitten, käytä sitä määrittääksesi ref komponentissasi:
const myRef = useRef(null);
Lopuksi, välitä se ref
-attribuuttina JSX-tagille, jonka DOM-elementin haluat saada:
<div ref={myRef}>
useRef
Hookki palauttaa olion yhdellä current
propertyllä. Aluksi, myRef.current
on null
. Kun React luo DOM noodin tästä <div>
:stä, React asettaa viitteen tähän noodiin myRef.current
:iin. Voit sitten päästä käsiksi tähän DOM noodiin Tapahtumankäsittelijästäsi ja käyttää sisäänrakennettuja selaimen rajapintoja jotka siihen on määritelty.
// Voit käyttää mitä tahansa selaimen rajapintoja, esimerkiksi:
myRef.current.scrollIntoView();
Esimerkki: Tekstikentän kohdentaminen
Tässä esimerkissä, tekstikenttä kohdentuu klikkaamalla nappia:
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Toteuttaaksesi tämän:
- Määritä
inputRef
useRef
Hookilla. - Välitä se seuraavasti
<input ref={inputRef}>
. Tämä pyytää Reactia asettamaan tämän<input>
:n DOM noodininputRef.current
:iin. handleClick
Tapahtumankäsittelijässä, lue tekstikentän DOM noodiinputRef.current
:sta ja kutsu senfocus()
funktiota,inputRef.current.focus()
:lla.- Välitä
handleClick
Tapahtumankäsittelijä<button>
:lleonClick
attribuutilla.
Vaikka DOM manipulaatio on yleisin käyttötapaus refseille, useRef
Hookia voidaan myös käyttää tallentamaan Reactin ulkopuolella olevia asioita, kuten ajastimien ID:tä. Juuri kuten tila, refit pysyvät renderöintien välillä. Refit ovat tilamuuttujia, jotka eivät aiheuta uudelleenrenderöintiä, kun niitä asetetaan. Lue refien esittely: Viittausten käyttö Refseillä.
Esimerkki: Scrollaaminen elementtiin
Sinulla voi olla enemmän kuin yksi ref komponentissa. Tässä esimerkissä on karuselli kolmesta kuvasta. Jokainen nappi keskittää kuvan kutsumalla vastaavan DOM noodin scrollIntoView()
metodia vastaavalla DOM noodilla:
import { useRef } from 'react'; export default function CatFriends() { const firstCatRef = useRef(null); const secondCatRef = useRef(null); const thirdCatRef = useRef(null); function handleScrollToFirstCat() { firstCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function handleScrollToSecondCat() { secondCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function handleScrollToThirdCat() { thirdCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } return ( <> <nav> <button onClick={handleScrollToFirstCat}> Tom </button> <button onClick={handleScrollToSecondCat}> Maru </button> <button onClick={handleScrollToThirdCat}> Jellylorum </button> </nav> <div> <ul> <li> <img src="https://placekitten.com/g/200/200" alt="Tom" ref={firstCatRef} /> </li> <li> <img src="https://placekitten.com/g/300/200" alt="Maru" ref={secondCatRef} /> </li> <li> <img src="https://placekitten.com/g/250/200" alt="Jellylorum" ref={thirdCatRef} /> </li> </ul> </div> </> ); }
Syväsukellus
Yllä olevissa esimerkeissä on määritelty valmiiksi refsejä. Joskus kuitenkin tarvitset refin jokaiseen listan kohteeseen, ja et tiedä kuinka monta niitä on. Seuraavanlainen koodi ei toimi:
<ul>
{items.map((item) => {
// Ei toimi!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Tämä tapahtuu koska Hookit on kutsuttava vain komponentin ylimmällä tasolla. Et voi kutsua useRef
:ia silmukassa, ehtolauseessa tai map()
kutsussa.
Yksi mahdollinen tapa ratkaista tämä on hakea yksi ref ylemmälle elementille, ja käyttää sitten DOM manipulaatiomenetelmiä kuten querySelectorAll
löytääksesi yksittäiset lapsinoodit. Tämä on kuitenkin herkkä ja voi rikkoutua, jos DOM-rakenne muuttuu.
Toinen mahdollinen ratkaisu on välittää funktio ref
attribuuttiin. Tätä kutsutaan ref
callbackiksi. React kutsuu ref-callbackkia DOM noodilla kun on aika asettaa ref, ja null
:lla kun se on aika tyhjentää se. Tämä mahdollistaa omien taulukoiden tai Map:n ylläpidon, ja mahdollistaa refin hakemisen indeksin tai jonkinlaisen ID:n perusteella.
Tämä esimerkki näyttää miten voit käyttää tätä menetelmää scrollataksesi mihin tahansa kohtaan pitkässä listassa:
import { useRef } from 'react'; export default function CatFriends() { const itemsRef = useRef(null); function scrollToId(itemId) { const map = getMap(); const node = map.get(itemId); node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function getMap() { if (!itemsRef.current) { // Alusta Map ensimmäisellä kerralla. itemsRef.current = new Map(); } return itemsRef.current; } return ( <> <nav> <button onClick={() => scrollToId(0)}> Tom </button> <button onClick={() => scrollToId(5)}> Maru </button> <button onClick={() => scrollToId(9)}> Jellylorum </button> </nav> <div> <ul> {catList.map(cat => ( <li key={cat.id} ref={(node) => { const map = getMap(); if (node) { map.set(cat.id, node); } else { map.delete(cat.id); } }} > <img src={cat.imageUrl} alt={'Cat #' + cat.id} /> </li> ))} </ul> </div> </> ); } const catList = []; for (let i = 0; i < 10; i++) { catList.push({ id: i, imageUrl: 'https://placekitten.com/250/200?image=' + i }); }
Tässä esimerkissä itemsRef
ei sisällä yhtäkään DOM noodia. Sen sijaan se sisältää Map:n, jossa on jokaisen kohteen ID ja DOM noodi. (Refseissä voi olla mitä tahansa arvoja!) Jokaisen listan kohteen ref
callback huolehtii siitä, että Map päivitetään:
<li
key={cat.id}
ref={node => {
const map = getMap();
if (node) {
// Lisää Map:iin
map.set(cat.id, node);
} else {
// Poista Map:sta
map.delete(cat.id);
}
}}
>
Tämän avulla voit lukea yksittäiset DOM noodit Map:sta myöhemmin.
Pääsy toisen komponentin DOM-noodiin
Kun asetat refin sisäänrakennettuun komponenttiin, joka tuottaa selaimen elementin kuten <input />
:n, React asettaa refin current
propertyn vastaamaan DOM noodia (kuten todellista <input />
:ia selaimessa).
Kuitenkin, jos yrität asettaa refin omalle komponentillesi, kuten <MyInput />
, oletuksena saat null
:n. Näet sen tässä esimerkissä. Huomaa miten painikkeen painaminen ei keskitä inputia:
import { useRef } from 'react'; function MyInput(props) { return <input {...props} />; } export default function MyForm() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Helpottaaksesi ongelman havaitsemista, React tulostaa myös virheen konsoliin:
Tämä tapahtuu, koska oletuksena React ei anna komponenttien päästä muiden komponenttien DOM noodeihin käsiksi. Ei edes omille lapsille! Tämä on tarkoituksellista. Refit ovat pelastusluukku, jota pitäisi käyttää niukasti. Toisen komponentin DOM noodin käsin manipulaatio tekee koodistasi vieläkin hauraamman.
Sen sijaan, komponentit jotka haluavat antaa muille pääsyn DOM noodehin, täytyy niiden eksplisiittisesti ottaa käyttöön tämä toiminto. Komponentti voi määrittää, että se “välittää” sen refit yhdelle lapsistaan. Tässä on tapa, jolla MyInput
voi käyttää forwardRef
API:a:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
Tässä miten se toimii:
<MyInput ref={inputRef} />
kertoo Reactille että asettaa vastaavan DOM noodininputRef.current
:iin. Kuitenkin, on seMyInput
komponentin vastuulla ottaa tämä käyttöön—oletuksena se ei tee sitä.MyInput
komponentti on määritelty käyttäenforwardRef
:ia. Tämä antaa sen vastaanottaainputRef
:in yllä olevastaref
argumentista, joka on määriteltyprops
:n jälkeen.MyInput
komponentti välittää saamansaref
:n sen sisällä olevalle<input>
komponentille.
Nyt painikkeen painaminen keskittää inputin:
import { forwardRef, useRef } from 'react'; const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Design-järjestelmissä yleinen malli on, että alhaisen tason komponentit kuten painikkeet, inputit ja muut, välittävät refit DOM noodeihinsa. Toisaalta, korkean tason komponentit kuten lomakkeet, listat tai sivun osat eivät yleensä välitä DOM noodejaan, jotta välteittäisiin tahallinen riippuvuus DOM rakenteesta.
Syväsukellus
Yllä olevassa esimerkissä MyInput
julkaisee alkuperäisen DOM input elementin. Tämä mahdollistaa ylemmän tason komponentin kutsun focus()
:iin. Kuitenkin, tämä mahdollistaa myös sen, että ylemmän tason komponentti voi tehdä jotain muuta—esimerkiksi muuttaa sen CSS tyylejä. Harvoin tapahtuvissa tapauksissa, saatat haluta rajoittaa julkistettua toiminnallisuutta. Voit tehdä sen useImperativeHandle
:n avulla:
import { forwardRef, useRef, useImperativeHandle } from 'react'; const MyInput = forwardRef((props, ref) => { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // Julkaise vain focus eikä mitään muuta focus() { realInputRef.current.focus(); }, })); return <input {...props} ref={realInputRef} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Tässä, MyInput
:n sisällä realInputRef
sisältää oikean input DOM noodin. Kuitenkin, useImperativeHandle
ohjeistaa Reactia antamaan oman erityisen olion refin arvona ylemmälle komponentille. Joten Form
:n sisällä inputRef.current
pitää sisällään vain focus
metodin. Tässä tapauksessa, ref “handle” ei ole DOM noodi, vaan oma olio, joka luotiin useImperativeHandle
kutsussa.
Kun React liittää refit
Reactissa jokainen päivitys on jaettu kahteen vaiheeseen:
- Renderöinnin aikana React kutsuu komponenttisi selvittääksesi mitä pitäisi näkyä ruudulla.
- Kommitoinnin aikana React ottaa muutokset käyttöön DOM:ssa.
Yleensä ei kannata käyttää refseja renderöinnin aikana. Tämä koskee myös refseja, jotka sisältävät DOM noodeja. Ensimmäisellä renderöinnillä, DOM noodeja ei ole vielä luotu, joten ref.current
on null
. Ja päivitysten renderöinnin aikana, DOM noodeja ei ole vielä päivitetty. Joten on liian aikaista lukea niitä.
React asettaa ref.current
:n kommitoinnin aikana. Ennen DOM:n päivittämistä, React asettaa ref.current
arvot null
:ksi. Päivittämisen jälkeen, React asettaa ne välittömästi vastaaviin DOM noodeihin.
Useiten saatat käyttää refseja Tapahtumankäsittelijöiden sisällä. Jos haluat tehdä jotain refin kanssa, mutta ei ole tiettyä tapahtumaa jota käyttää, saatat tarvita Effektiä. Seuraavilla sivuilla käymme läpi Effektin.
Syväsukellus
Harkitse seuraavaa koodia, joka lisää uuden tehtävän listaan ja selaa ruudun listan viimeiseen lapsinoodiin. Huomaa, miten jostain syystä se aina selaa tehtävään, joka oli juuri ennen viimeksi lisättyä:
import { useState, useRef } from 'react'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; setText(''); setTodos([ ...todos, newTodo]); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Add </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
Ongelma on näiden kahden rivin kanssa:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
Reactissa, tilapäivitykset ovat jonossa. Useiten tämä on haluttu toiminto. Kuitenkin tässä tapauksessa se aiheuttaa ongelman, koska setTodos
ei päivitä DOM:ia välittömästi. Joten aikana jolloin listaa selataan viimeiseen elementtiin, tehtävää ei ole vielä lisätty. Tästä syystä, scrollaus “jää” aina yhden elementin jälkeen.
Korjataksesi tämän ongelman, voit pakottaa Reactin päivittämään (“flush”) DOM:n synkronisesti. Tämän saa aikaan tuomalla flushSync
:n react-dom
kirjastosta ja ympäröimällä tilapäivityksen flushSync
kutsulla:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
Tämä ohjeistaa Reactia päivittämään DOM:n synkronisesti heti flushSync
:n ympäröimän koodin suorituksen jälkeen. Tämän seurauksena, viimeinen tehtävä on jo DOM:ssa, kun yrität scrollata siihen:
import { useState, useRef } from 'react'; import { flushSync } from 'react-dom'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; flushSync(() => { setText(''); setTodos([ ...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Add </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
Parhaat käytännöt DOM-manipulaatioon refeillä
Refit ovat pelastusluukku. Niitä tulisi käyttää vain kun sinun täytyy “astua Reactin ulkopuolelle”. Yleisiä esimerkkejä tästä ovat kohdentamisesta, scrollaamisesta tai selaimen API:sta, jota React ei tarjoa.
Jos pysyttelet ei-destruktivisissa toiminnoissa kuten kohdentamisessa ja scrollaamisessa, sinun ei tulisi törmätä ongelmiin. Kuitenkin, jos yrität muokata DOM:ia manuaalisesti, saatat riskeerata ristiriidan Reactin tekemien muutosten kanssa.
Ongelman kuvainnollistamiseksi, tämä esimerkki sisältää tervetuloviestin sekä kaksi painiketta. Ensimmäinen painike vaihtaa sen näkyvyyttä käyttäen ehdollista renderöintiä ja tilaa, kuten yleensä Reactissa on tapana. Toinen painike käyttää remove()
DOM API:a poistaakseen sen DOM:ista väkisin Reactin ulkopuolella.
Kokeile painamalla “Toggle with setState” painiketta muutaman kerran. Viestin pitäisi hävitä ja ilmestyä uudelleen. Paina sitten “Remove from the DOM”. Tämä poistaa sen väkisin. Lopuksi paina “Toggle with setState”:
import { useState, useRef } from 'react'; export default function Counter() { const [show, setShow] = useState(true); const ref = useRef(null); return ( <div> <button onClick={() => { setShow(!show); }}> Toggle with setState </button> <button onClick={() => { ref.current.remove(); }}> Remove from the DOM </button> {show && <p ref={ref}>Hello world</p>} </div> ); }
Manuaalisen DOM elementin poiston jälkeen, kokeile setState
:n käyttöä näyttääksesi sen uudelleen. Tämä johtaa kaatumiseen. Tämä johtuu siitä, että olet muuttanut DOM:ia, ja React ei tiedä miten jatkaa sen hallintaa oikein.
Vältä Reactin hallinnoimien DOM elementtien muuttamista. Reactin hallinnoimien elementtien muuttaminen, lasten lisääminen tai lasten poistaminen voi johtaa epäjohdonmukaisiin näkymiin tai kaatumisiin kuten yllä.
However, this doesn’t mean that you can’t do it at all. It requires caution. You can safely modify parts of the DOM that React has no reason to update. For example, if some <div>
is always empty in the JSX, React won’t have a reason to touch its children list. Therefore, it is safe to manually add or remove elements there.
Kuitenkin, tämä ei tarkoita, etteikö sitä voisi tehdä ollenkaan. Tämä vaatii varovaisuutta. Voit turvallisesti muokata osia DOM:ista, joita React:lla ei syytä päivittää. Esimerkiksi, jos jokin <div>
on aina tyhjä JSX:ssä, Reactilla ei ole syytä koskea sen lasten listaan. Näin siinä on turvallista manuaalisesti lisätä tai poistaa elementtejä.
Kertaus
- Refit ovat yleinen konsepti, mutta yleensä käytät niitä pitämään DOM elementtejä.
- Ohjeistat Reactia laittamaan DOM noodin
myRef.current
-propertyyn<div ref={myRef}>
:lla. - Useiten, käytät refejä ei-destruktivisille toiminnoille kuten kohdentamiselle, scrollaamiselle tai DOM elementtien mitoittamiselle.
- Komponentti ei julkaise sen DOM noodia oletuksena. Voit julkaista DOM noodin käyttämällä
forwardRef
:ia ja välittämällä toisenref
-argumentin alas tiettyyn noodiin. - Vältä Reactin hallinnoimien DOM elementtien muuttamista.
- Mikäli muokkaat Reactin hallinnoimaa DOM noodia, muokkaa osia, joita Reactilla ei ole syytä päivittää.
Haaste 1 / 4: Toista ja pysäytä video
In this example, the button toggles a state variable to switch between a playing and a paused state. However, in order to actually play or pause the video, toggling state is not enough. You also need to call play()
and pause()
on the DOM element for the <video>
. Add a ref to it, and make the button work.
Tässä esimerkissä, painike vaihtaa tilamuuttujaa vaihtaakseen toistamisen ja pysäytetyn tilan välillä. Kuitenkin, jotta video oikeasti toistuisi tai pysähtyisi, tilan vaihtaminen ei riitä. Sinun täytyy myös kutsua <video>
DOM elementin play()
ja pause()
funktioita. Lisää ref elementille, ja tee painike toimivaksi.
import { useState, useRef } from 'react'; export default function VideoPlayer() { const [isPlaying, setIsPlaying] = useState(false); function handleClick() { const nextIsPlaying = !isPlaying; setIsPlaying(nextIsPlaying); } return ( <> <button onClick={handleClick}> {isPlaying ? 'Pause' : 'Play'} </button> <video width="250"> <source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4" /> </video> </> ) }
Lisähaasteena, pidä “Play” painike synkronoituna videon toiston tilan kanssa, vaikka käyttäjä klikkaisi videota hiiren oikealla painikkeella ja toistaa sen käyttämällä selaimen sisäisiä media-ohjauksia. Saatat tarvita Tapahtumankäsittelijää onPlay
ja onPause
video-elementillä tämän toteuttaaksesi.