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 Hook useRef
  • 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:

  1. Deklarasikan inputRef dengan useRef Hook.
  2. Oper sebagai <input ref={inputRef}>. React akan meletakkan <input> simpul DOM ke dalam inputRef.current.
  3. Pada fungsi handleClick, baca input simpul DOM dari inputRef.current dan panggil focus() menggunakan inputRef.current.focus().
  4. Oper handleClick event handler ke <button> menggunakan onClick.

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

Bagaimana mengatur daftar ref menggunakan ref callback

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 } 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 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.id, node);
} else {
// Remove from the Map
map.delete(cat.id);
}
}}
>

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:

Konsol
Perhatian: fungsional komponen tidak dapat menerima ref. Mencoba mengaksesnya akan gagal. Apakah yang Anda maksud React.forwardRef()?

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:

  1. <MyInput ref={inputRef} /> memberitahu React untuk meletakkan simpul DOM yang sesuai ke dalam inputRef.current. Namun, itu terserah komponen MyInput untuk mengikutinya—secara default, tidak.
  2. Komponen MyInput menggunakan forwardRef. Dengan ini komponen menerima inputRef dari atas sebagai argumen ref kedua yang dideklarasikan setelah props.
  3. MyInput itu sendiri mengoper ref 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

Ekspos bagian dari API dengan imperatif handle

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 cara tertentu untuk melakukannya, Anda mungkin memerlukan Effect. Kami akan membahas effect pada halaman berikutnya.

Pendalaman

Mengosongkan pembaruan state secara sinkron dengan flushSync

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 argumen ref 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.