Memanipulasi DOM dengan Refs
React secara otomatis memperbarui DOM (Document Object Model) agar sama dengan render output Anda, dengan begitu komponen Anda tidak akan sering membutuhkan manipulasi. Namun, terkadang Anda mungkin butuh mengakses elemen DOM yang diatur React—sebagai contoh, fokus ke sebuah node, gulir, atau menentukan posisi dan ukurannya. Tidak ada cara bawaan untuk melakukannya dalam React, jadi Anda butuh ref pada node DOM.
You will learn
- Bagaimana cara mengakses sebuah node DOM yang diatur React dengan atribut
ref
- Bagaimana atribut JSX
ref
berelasi dengan HookuseRef
- Bagaimana cara mengakses node DOM dari komponen lain
- Dalam kasus seperti apa memodifikasi DOM yang diatur React dengan aman.
Menempatkan Ref ke sebuah node
Untuk mengakses sebuah node DOM yang diatur React, pertama, impor Hook useRef
:
import { useRef } from 'react';
Kemudian, deklarasikan ref di dalam komponen Anda:
const myRef = useRef(null);
Terakhir, oper ke DOM node sebagai atribut ref
:
<div ref={myRef}>
Hook useRef
mengembalikan objek dengan properti tunggal bernama current
. Awalnya, myRef.current
akan bernilai null
. Saat React membuat node DOM untuk <div>
, React akan mereferensikan node ini ke dalam myRef.current
. Anda bisa mengakses node DOM ini dari event handlers Anda dan menggunakan API peramban bawaan.
// Anda dapat menggunakan API peramban apa saja, sebagai contoh:
myRef.current.scrollIntoView();
Contoh: Fokus ke sebuah input teks
Pada contoh ini, meng-klik tombol tersebut akan fokus ke input:
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> </> ); }
Penerapannya:
- Deklarasikan
inputRef
denganuseRef
Hook. - Oper sebagai
<input ref={inputRef}>
. React akan meletakkan<input>
DOM node ke dalaminputRef.current
. - Pada fungsi
handleClick
, baca input DOM node dariinputRef.current
dan panggilfocus()
menggunakaninputRef.current.focus()
. - Oper
handleClick
event handler ke<button>
menggunakanonClick
.
Mesikpun manipulasi DOM merupakan hal yang sangat umum untuk refs, Hook useRef
dapat digunakan untuk menyimpan hal-hal lain di luar React, seperti ID timer. Sama halnya dengan state, refs tetap sama di antara proses render. Refs sama seperti variabel state yang tidak memicu banyak render saat Anda mengaturnya. Baca tentang refs pada Mereferensikan nilai dengan Refs.
Contoh: Menggulir ke sebuah Elemen
Anda dapat memiliki lebih dari satu ref dalam sebuah komponen. Pada contoh, terdapat carousel terdiri dari 3 gambar. Setiap tombol mengarahkan ke pusat gambar dengan memanggil metode peramban scrollIntoView()
pada node DOM terkait:
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> </> ); }
Deep Dive
Pada contoh di atas, terdapat beberapa refs yang telah didefinisikan. Namun, terkadang Anda mungkin butuh ref pada tiap item dalam daftar, dan Anda tidak tahu berapa banyak yang akan Anda dapatkan. Hal seperti ini tidak akan berfungsi:
<ul>
{items.map((item) => {
// Tidak akan berfungsi
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Ini dikarenakan Hooks hanya dapat dipanggil di tingkat atas komponen Anda. Anda tidak dapat memanggil useRef
di dalam perulangan, di dalam kondisi, atau di dalam pemanggilanmap()
.
Satu cara yang memungkinkan adalah mendapatkan ref tunggal dari elemen parent, lalu gunakan metode manipulasi DOM seperti querySelectorAll
untuk “menemukan” masing-masing child node. Namun, hal ini beresiko kacau jika struktur DOM Anda berubah.
Solusi lainnya adalah dengan mengoper fungsi pada atribut ref
. hal ini dinamakan ref
callback. React akan memanggil ref callback Anda dengan node DOM saat menyiapkan ref, dan dengan null
saat menghapusnya. hal ini memungkinkan anda mengatur Array Anda sendiri atau sebuah Map, dan mengakses ref dengan indeks atau sejenis ID.
Contoh di bawah menunjukan bagaimana Anda dapat menggunakan pendekatan ini untuk menggulir node di dalam daftar yang panjang:
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) { // menginisialisasi Map pada pertama kali penggunaan. 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 }); }
Pada contoh ini, itemsRef
tidak menyimpan node DOM. Namun,menyimpan sebuah Map dari ID item ke node DOM. (Refs dapat menyimpan nilai apapun!) Ref
callback pada tiap daftar memperhatikan pembaruan Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
if (node) {
// Add to the Map
map.set(cat.id, node);
} else {
// Remove from the Map
map.delete(cat.id);
}
}}
>
Hal ini memungkinkan Anda membaca node DOM individu dari Map nanti.
Mengakses node DOM komponen lain
Saat Anda memasang ref pada komponen bawaan yang mana hasilnya adalah elemen peramban seperti <input />
, React akan mengatur properti current
ref pada node DOM tersebut (seperti aktual <input />
pada peramban).
Namun, jika Anda mencoba mengatur ref pada komponen Anda sendiri, seperti <MyInput />
, secara default Anda akan mendapatkan null
. Ini contohnya. Perhatikan bagaimana meng-klik tombol tidak membuat fokus pada input:
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> </> ); }
Untuk membantu Anda menanggapi issue tersebut, React juga mencetak error pada console:
Ini terjadi karena secara default React tidak mengizinkan komponen mengakses node DOM dari komponen lain. Bahkan children dari komponen tersebut! ini ada alasannya. Refs merupakan jalan darurat yang seharusnya jarang digunakan. Memanipulasi node DOM komponen lain secara manual membuat kode Anda lebih rentan.
Alih-alih, komponen yang ingin mengekspos node DOM harus mengikut perilaku tersebut. Sebuah komponen dapat menentukan untuk meneruskan ref ke salah satu childrennya. Contoh bagaimana MyInput
dapat menggunakan API forwardRef
:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
Ini cara kerjanya:
<MyInput ref={inputRef} />
memberitahu React untuk meletakkan node DOM yang sesuai ke dalaminputRef.current
. Namun, itu terserah komponenMyInput
untuk mengikutinya—secara default, tidak.- Komponen
MyInput
menggunakanforwardRef
. Dengan ini komponen menerimainputRef
dari atas sebagai argumenref
kedua yang dideklarasikan setelahprops
. MyInput
itu sendiri mengoperref
yang diterima ke dalam<input>
.
Sekarang meng-klik tombol untuk fokus ke input berfungsi:
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> </> ); }
Dalam sistem desain, merupakan pola umum untuk komponen level rendah seperti tombol, input dan sejenisnya untuk meneruskan ref ke node DOM. Sebaliknya, komponen level atas seperti form,list atau seksi halaman biasanya tidak mengekspos node DOM untuk menghindari dependensi yang tidak disengaja pada struktur DOM.
Deep Dive
Pada contoh di atas, MyInput
mengekspos elemen input DOM. Ini memungkinkan komponen parent memanggil focus()
. Namun, ini juga memungkinkan parent komponen melakukan hal lain—contohnya, mengubah CSS (Cascading Style Sheet) style. Dalam kasus umum, Anda mungkin ingin membatasi fungsionalitas yang akan diekspos. Anda dapat melakukannya dengan useImperativeHandle
:
import { forwardRef, useRef, useImperativeHandle } from 'react'; const MyInput = forwardRef((props, ref) => { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // Only expose focus and nothing else 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> </> ); }
Di sini, realInputRef
di dalam MyInput
memegang aktual node DOM input. Namun, useImperativeHandle
menginstruksikan React untuk menyediakan spesial obyek tersendiri sebagai nilai dari ref ke komponen parent. Jadi inputRef.current
di dalam komponen Form
hanya akan memiliki metode focus
. Dalam kasus ini, “handle” ref bukan node DOM, tetap kustom obyek yang Anda buat di dalam pemanggilan useImperativeHandle
.
Ketika React menyematkan Ref
Dalam React, setiap pembaharuan dibagi menjadi dua fase:
- Selama proses render, React memanggil komponen Anda untuk mencari tahu apa yang seharusnya berada di layar.
- Selama proses commit, React menerapkan perubahan pada DOM.
Secara umum, Anda tidak ingin mengakses ref selama proses rendering. Keadaan dimana ref menyimpan node DOM. Selama proses render pertama, node DOM belum dibuat, jadi ref.current
bernilai null
. Dan selama proses pembaharuan render, node DOM belum diperbarui. Jadi terlalu awal untuk membacanya.
React sets ref.current
during the commit. Before updating the DOM, React sets the affected ref.current
values to null
. After updating the DOM, React immediately sets them to the corresponding DOM nodes.
Usually, you will access refs from event handlers. If you want to do something with a ref, but there is no particular event to do it in, you might need an Effect. We will discuss effects on the next pages.
Deep Dive
Consider code like this, which adds a new todo and scrolls the screen down to the last child of the list. Notice how, for some reason, it always scrolls to the todo that was just before the last added one:
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) }); }
The issue is with these two lines:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
In React, state updates are queued. Usually, this is what you want. However, here it causes a problem because setTodos
does not immediately update the DOM. So the time you scroll the list to its last element, the todo has not yet been added. This is why scrolling always “lags behind” by one item.
To fix this issue, you can force React to update (“flush”) the DOM synchronously. To do this, import flushSync
from react-dom
and wrap the state update into a flushSync
call:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
This will instruct React to update the DOM synchronously right after the code wrapped in flushSync
executes. As a result, the last todo will already be in the DOM by the time you try to scroll to it:
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) }); }
Best practices for DOM manipulation with refs
Refs are an escape hatch. You should only use them when you have to “step outside React”. Common examples of this include managing focus, scroll position, or calling browser APIs that React does not expose.
If you stick to non-destructive actions like focusing and scrolling, you shouldn’t encounter any problems. However, if you try to modify the DOM manually, you can risk conflicting with the changes React is making.
To illustrate this problem, this example includes a welcome message and two buttons. The first button toggles its presence using conditional rendering and state, as you would usually do in React. The second button uses the remove()
DOM API to forcefully remove it from the DOM outside of React’s control.
Try pressing “Toggle with setState” a few times. The message should disappear and appear again. Then press “Remove from the DOM”. This will forcefully remove it. Finally, press “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> ); }
After you’ve manually removed the DOM element, trying to use setState
to show it again will lead to a crash. This is because you’ve changed the DOM, and React doesn’t know how to continue managing it correctly.
Avoid changing DOM nodes managed by React. Modifying, adding children to, or removing children from elements that are managed by React can lead to inconsistent visual results or crashes like above.
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.
Recap
- Refs are a generic concept, but most often you’ll use them to hold DOM elements.
- You instruct React to put a DOM node into
myRef.current
by passing<div ref={myRef}>
. - Usually, you will use refs for non-destructive actions like focusing, scrolling, or measuring DOM elements.
- A component doesn’t expose its DOM nodes by default. You can opt into exposing a DOM node by using
forwardRef
and passing the secondref
argument down to a specific node. - Avoid changing DOM nodes managed by React.
- If you do modify DOM nodes managed by React, modify parts that React has no reason to update.
Challenge 1 of 4: Play and pause the 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.
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> </> ) }
For an extra challenge, keep the “Play” button in sync with whether the video is playing even if the user right-clicks the video and plays it using the built-in browser media controls. You might want to listen to onPlay
and onPause
on the video to do that.