Memanipulasi DOM dengan Refs
React secara otomatis memperbarui DOM (Document Object Model) agar sesuai dengan keluaran render, sehingga komponen Anda tidak perlu sering memanipulasinya. Namun, terkadang Anda mungkin perlu mengakses elemen DOM yang dikelola oleh React—misalnya, untuk memberikan fokus pada sebuah simpul (node), menggulir ke sana, atau mengukur ukuran dan posisinya. Tidak ada cara bawaan untuk melakukan hal-hal tersebut di React, sehingga Anda memerlukan ref ke simpul DOM.
Anda akan mempelajari
- Bagaimana cara mengakses sebuah simpul DOM yang diatur React dengan atribut
ref
- Bagaimana atribut JSX
ref
berelasi dengan HookuseRef
- Bagaimana cara mengakses simpul DOM dari komponen lain
- Dalam kasus seperti apa memodifikasi DOM yang diatur React dengan aman.
Mendapatkan sebuah ref untuk simpul
Untuk mengakses sebuah simpul 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 ref Anda sebagai atribut ref
ke tag JSX yang Anda ingin dapatkan simpul DOM-nya:
<div ref={myRef}>
Hook useRef
mengembalikan objek dengan properti tunggal bernama current
. Awalnya, myRef.current
akan bernilai null
. Saat React membuat simpul DOM untuk <div>
, React akan mereferensikan simpul ini ke dalam myRef.current
. Anda bisa mengakses simpul 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>
simpul DOM ke dalaminputRef.current
. - Pada fungsi
handleClick
, baca input simpul DOM dariinputRef.current
dan panggilfocus()
menggunakaninputRef.current.focus()
. - Oper
handleClick
event handler ke<button>
menggunakanonClick
.
Mesikpun manipulasi DOM merupakan hal yang sangat umum untuk ref, Hook useRef
dapat digunakan untuk menyimpan hal-hal lain di luar React, seperti ID timer. Sama halnya dengan state, ref tetap sama di antara proses render. Ref sama seperti variabel state yang tidak memicu banyak render saat Anda mengaturnya. Baca tentang ref pada Mereferensikan nilai dengan Ref.
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 simpul 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> </> ); }
Pendalaman
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 simpul 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 simpul di dalam daftar yang panjang:
import { useRef, useState } from "react"; export default function CatFriends() { const itemsRef = useRef(null); const [catList, setCatList] = useState(setupCatList); function scrollToCat(cat) { const map = getMap(); const node = map.get(cat); 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={() => scrollToCat(catList[0])}>Tom</button> <button onClick={() => scrollToCat(catList[5])}>Maru</button> <button onClick={() => scrollToCat(catList[9])}>Jellylorum</button> </nav> <div> <ul> {catList.map((cat) => ( <li key={cat} ref={(node) => { const map = getMap(); if (node) { map.set(cat, node); } else { map.delete(cat); } }} > <img src={cat} /> </li> ))} </ul> </div> </> ); } function setupCatList() { const catList = []; for (let i = 0; i < 10; i++) { catList.push("https://loremflickr.com/320/240/cat?lock=" + i); } return catList; }
Pada contoh ini, itemsRef
tidak menyimpan simpul DOM. Namun,menyimpan sebuah Map dari ID item ke simpul DOM. (Refs dapat menyimpan nilai apa pun!) 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, node);
} else {
// Remove from the Map
map.delete(cat);
}
}}
>
Hal ini memungkinkan Anda membaca simpul DOM individu dari Map nanti.
Mengakses simpul 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 simpul 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 simpul DOM dari komponen lain. Bahkan children dari komponen tersebut! ini ada alasannya. Ref merupakan jalan darurat yang seharusnya jarang digunakan. Memanipulasi simpul DOM komponen lain secara manual membuat kode Anda lebih rentan.
Sebaliknya, komponen yang ingin mengekspos simpul DOM harus memilih 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 simpul 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 simpul DOM. Sebaliknya, komponen level atas seperti form, list atau bagian dari halaman biasanya tidak mengekspos simpul DOM untuk menghindari dependensi yang tidak disengaja pada struktur DOM.
Pendalaman
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 simpul 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 simpul DOM, tetap kustom obyek yang Anda buat di dalam pemanggilan useImperativeHandle
.
Ketika React menyematkan Ref
Dalam React, setiap pembaruan 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 simpul DOM. Selama proses render pertama, simpul DOM belum dibuat, jadi ref.current
bernilai null
. Dan selama proses pembaharuan render, simpul DOM belum diperbarui. Jadi terlalu awal untuk membacanya.
React mengatur nilai ref.current
selama commit. Sebelum memperbarui DOM, React mengatur nilai ref.current
yang terpengaruh menjadi null
. Setelah memperbarui DOM, React segera mengatur nilai ref.current
tersebut menjadi simpul DOM yang sesuai.
Biasanya, Anda akan mengakses ref dari event handler. Jika Anda ingin melakukan sesuatu dengan sebuah ref, tetapi tidak ada event tertentu untuk melakukannya, Anda mungkin memerlukan Effect. Kami akan membahas effect pada halaman berikutnya.
Pendalaman
Perhatikan kode ini, yang menambahkan todo baru dan menggulirkan layar ke bawah hingga pada elemen terakhir dari daftar. Perhatikan bagaimana, dengan alasan tertentu, hal ini selalu menggulirkan layar ke todo yang berada sebelum yang terakhir ditambahkan.
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) }); }
Masalah terletak pada dua baris kode ini:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
Di React, pembaruan state dijadwalkan dalam antrian.. Biasanya, ini adalah yang diinginkan. Namun, ini menyebabkan masalah karena setTodos
tidak segera memperbarui DOM. Jadi, saat Anda menggulirkan daftar ke elemen terakhirnya, todo belum ditambahkan. Inilah sebabnya mengapa pengguliran selalu “terlambat” satu item.
Untuk memperbaiki masalah ini, Anda dapat memaksa React untuk memperbarui (“flush”) DOM secara sinkron. Untuk melakukan ini, impor flushSync
dari react-dom
dan bungkus pembaruan state dengan pemanggilan flushSync:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
Ini akan memberitahu React untuk memperbarui DOM secara sinkron tepat setelah kode yang dibungkus dalam flushSync
dieksekusi. Sebagai hasilnya, todo terakhir sudah ada di DOM pada saat Anda mencoba untuk menggulirnya:
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) }); }
Praktik terbaik memanipulasi DOM dengan refs.
Ref adalah jalan keluar. Anda harus hanya menggunakannya ketika Anda harus “keluar dari React”. Contoh umum dari hal ini termasuk mengelola fokus, posisi scroll, atau memanggil API peramban yang tidak diekspos React.
Jika Anda tetap menggunakan tindakan yang tidak merusak seperti fokus dan scrolling, Anda tidak akan mengalami masalah. Namun, jika Anda mencoba untuk memodifikasi DOM secara manual, Anda dapat berisiko konflik dengan perubahan yang dilakukan React.
Untuk mengilustrasikan masalah ini, contoh ini mencakup pesan selamat datang dan dua tombol. Tombol pertama menampilkan/menyembunyikan menggunakan conditional rendering dan state, seperti yang biasanya dilakukan di React. Tombol kedua menggunakan remove()
API DOM untuk menghapusnya secara paksa dari DOM di luar kendali React.
Coba tekan “Toggle with setState” beberapa kali. Pesan seharusnya menghilang dan muncul lagi. Kemudian tekan “Remove from the DOM”. Ini akan menghapusnya secara paksa. Kemudian, tekan “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> ); }
Setelah Anda menghapus elemen DOM secara manual, mencoba menggunakan setState
untuk menampilkannya lagi akan menyebabkan masalah. Hal ini terjadi karena Anda telah mengubah DOM, dan React tidak tahu bagaimana melanjutkan mengelolanya dengan benar.
Hindari mengubah simpul DOM yang dikelola oleh React. Memodifikasi, menambahkan children, atau menghapus children dari elemen yang dikelola oleh React dapat menyebabkan hasil visual yang tidak konsisten atau masalah seperti di atas.
Namun, ini tidak berarti bahwa Anda sama sekali tidak dapat melakukannya. Ini membutuhkan kewaspadaan. Anda dapat dengan aman mengubah bagian dari DOM yang tidak diperbarui oleh React dengan alasan apa pun. Misalnya, jika beberapa <div>
selalu kosong di JSX, React tidak akan memiliki alasan untuk menyentuh daftar children. Oleh karena itu, aman untuk menambahkan atau menghapus elemen secara manual di sana.
Rekap
- Ref adalah konsep yang umum, tetapi paling sering digunakan untuk menyimpan elemen-elemen DOM.
- Anda menginstruksikan React untuk menempatkan simpul DOM ke
myRef.current
dengan mengoper<div ref={myRef}>
. - Biasanya, Anda akan menggunakan ref untuk tindakan non-destruktif seperti fokus, scrolling, atau mengukur elemen-elemen DOM.
- Komponen tidak secara default mengekspos simpul DOM-nya. Anda dapat memilih untuk mengekspos simpul DOM dengan menggunakan
forwardRef
dan mengoper argumenref
kedua ke simpul yang spesifik. - Hindari mengubah simpul DOM yang dikelola oleh React.
- Jika Anda mengubah simpul DOM yang dikelola oleh React, ubah bagian yang tidak perlu diperbarui oleh React.
Tantangan 1 dari 4: Putar dan jeda video
Dalam contoh ini, tombol mengalihkan state variabel untuk beralih antara state diputar atau dijeda. Namun, untuk benar-benar memutar atau menjeda video, mengalihkan state saja tidak cukup. Anda juga perlu memanggil play()
dan pause()
pada elemen DOM untuk <video>
. Tambahkan sebuah ref pada elemen tersebut, dan buat tombol bekerja.
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> </> ) }
Untuk tantangan tambahan, usahakan tombol “Play” selalu sejalan dengan apakah video sedang diputar meskipun pengguna mengklik kanan video dan memutarnya menggunakan kontrol media bawaan peramban. Anda mungkin ingin memperhatikan onPlay
dan onPause
pada video untuk melakukan hal tersebut.