Ref'ler ile DOM Manipülasyonu

React, DOM’u render edilen çıktıya uyacak şekilde otomatik olarak günceller. Böylece bileşenlerinizin genellikle onu değiştirmesi gerekmez. Ancak bazen React tarafından yönetilen DOM elemanlarına erişmeye ihtiyaç duyabilirsiniz örneğin bir elemana odaklamak, onu kaydırmak veya boyutunu ve konumunu ölçmek isteyebilirsiniz. React’te bunları yapmanın yerleşik bir yolu yoktur bu yüzden DOM elemanı için ref’e ihtiyacınız olacak.

Bunları öğreneceksiniz

  • React tarafından yönetilen bir DOM elemanına ref özelliğiyle nasıl erişilir?
  • JSX özelliği olan ref, useRef Hook’uyla nasıl ilişkilidir?
  • Başka bir bileşenin DOM elemanına nasıl erişilir?
  • Hangi durumlarda React tarafından yönetilen DOM’u değiştirmek güvenlidir?

Elemana ref alma

React tarafından yönetilen bir DOM elemanına erişmek için önce useRef Hook’unu içe aktarın:

import { useRef } from 'react';

Ardından bileşeninizin içinde bir ref bildirmek için kullanın:

const myRef = useRef(null);

Son olarak DOM elemanını almak istediğiniz JSX etiketine ref özelliği olarak ref’inizi iletin:

<div ref={myRef}>

useRef Hook’u current adlı tek bir özelliğe sahip bir nesne döndürür. Başlangıçta myRef.current, null olacaktır. React bu <div> için bir DOM elemanı oluşturduğunda React bu elemanın içine myRef.current referansı koyacaktır. Daha sonra bu DOM elemanına olay yöneticinizden erişebilir ve yerleşik tarayıcı API’lerini kullanabilirsiniz.

// Herhangi bir tarayıcı API'sini kullanabilirsiniz, örneğin:
myRef.current.scrollIntoView();

Örnek: Bir metin girişine odaklanma

Bu örnekte butona tıklamak input alanına odaklayacaktır:

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>
    </>
  );
}

Bunu uygulamak için:

  1. useRef Hook’u ile inputRef’i bildirin.
  2. <input ref={inputRef}> olarak geçin. React’e bu <input>’ta DOM elemanının içine inputRef.current’i koymasını söyler.
  3. handleClick fonksiyonunda inputRef.current’tan input DOM elemanını okuyun ve focus() ile inputRef.current.focus() ögesini çağırın.
  4. handleClick olay yöneticisini onClick ile <button> elemanına geçin.

DOM manipülasyonu ref için en yaygın kullanım olsa da useRef Hook’u, zamanlayıcı ID’ler gibi, React dışında başka şeyleri saklamak için de kullanılabilir. State’e benzer şekilde refler de renderlar arasında kalır. Ref, ayarladığınızda yeniden render etmeyi tetiklemeyen state değişkenleri gibidir. Ref hakkında bilgi edinin: Referencing Values with Refs.

Örnek: Bir öğeye scroll etmek

Bir bileşende birden fazla ref olabilir. Bu örnekte üç resimden oluşan bir carousel vardır. Her buton karşılık gelen ilgili DOM elemanında tarayıcıya scrollIntoView() metodunu çağırarak resmi ortalar.

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>
    </>
  );
}

Derinlemesine İnceleme

Ref callback kullanarak bir ref listesi nasıl yönetilir?

Yukarıdaki örneklerde önceden tanımlanmış sayıda ref vardır. Ancak bazen listedeki her bir öge için ref’e ihtiyacınız olabilir ve kaç tane olacağını bilemeyebilirsiniz. Böyle bir şey işe yaramaz:

<ul>
{items.map((item) => {
// Çalışmaz!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>

Bunun nedeni Hook’ların bileşeninizin sadece en üst seviyesinde çağrılması gerekmesinden kaynaklıdır. Bir döngüde, koşulda veya map()’in içinde useRef’i çağıramazsınız.

Bunun olası bir yolu ana elemana tek bir ref almak ve ardından tek tek alt elemanı bulmak için querySelectorAll gibi DOM manipülasyon yöntemlerini kullanmaktır. Ancak bu yöntem tutarsızdır ve DOM yapınız değişirse işlevsiz hale gelebilir.

Başka bir çözüm bir fonksiyonu ref özelliğine iletmektir. Buna ref callback denir. React ref’i ayarlama zamanı geldiğinde callback fonksiyonunu DOM elemanı ile çağıracak ve ref’i temizleme zamanı geldiğinde null değeri ile çağıracaktır. Bu, kendi dizinizi veya Map’inizi korumanıza ve indeksine veya kimliğine göre herhangi bir ref’e erişmenize olanak sağlar.

Bu örnek uzun bir listede rastgele bir elemana kaydırmak için bu yaklaşımı nasıl kullanabileceğimizi gösterir:

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) {
      // Map'i ilk kullanımda başlatın.
      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
  });
}

Bu örnekte itemsRef tek bir DOM elemanını tutmaz. Bunun yerine öge kimliğinden DOM elemanına bir Map tutar. (Ref’ler herhangi bir değeri tutabilir!) Her liste ögesindeki ref callback’i Map’i güncellemeye özen gösterir:

<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);
}
}}
>

Bu daha sonra Map’ten tek tek DOM elemanlarını okumamıza olanak tanır.

Başka bir bileşenin DOM elemanlarına erişme

<input /> gibi bir tarayıcı elemanı çıktısı veren yerleşik bir bileşene ref koyduğunuzda React bu ref’in current özelliğini karşılık gelen DOM elemanına ayarlar ( tarayıcıdaki asıl <input /> gibi).

Ancak <MyInput /> gibi kendi bileşeninize ref koymaya çalışırsanız varsayılan olarak null değeri alırsınız. İşte bunu gösteren bir örnek. Butona tıklamanın input’a nasıl odaklamadığına dikkat edin:

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>
    </>
  );
}

Sorunu fark etmenize yardımcı olmak için React ayrıca konsola bir hata yazdırır:

Console
Uyarı: Fonksiyon bileşenlerine ref verilemez. Bu ref’e erişme girişimleri başarısız olacaktır. React.forwardRef()‘i mi kullanmak istediniz?

Bunun nedeni React’in varsayılan olarak bir bileşenin diğer bileşenlerin DOM elemanlarına erişmesine izin vermemesidir. Kendi alt elemanı için bile değil! Bu kasıtlı. Ref, az miktarda kullanılması gereken bir kaçış kapısıdır. another bileşeninin DOM elemanlarını manuel olarak manipüle etmek kodu işlevsiz hale getirebilir.

Bunun yerine DOM elemanlarını açığa çıkarmak isteyen bileşenlerin bu davranışı seçmesi gerekir. Bir bileşen ref’in alt elemanlarından birine “forwards” belirtebilir. MyInput, forwardRef API’sini şu şekilde kullanabilir:

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

Çalışma şekli şöyledir:

  1. <MyInput ref={inputRef} /> React’e, karşılık gelen DOM elemanını inputRef.current’a koymasını söyler. Ancak bunu seçmek MyInput bileşenine bağlıdır. Varsayılan olarak bunu yapmaz.
  2. MyInput bileşeni forwardRef kullanılarak tanımlanır. props’tan sonra bildirilen ikinci ref, parametre olarak yukarıdan inputRef’i almayı seçer.
  3. MyInput aldığı ref’i içindeki <input>’a iletir.

Şimdi input’a odaklamak için butona tıklamak işe yarar:

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>
    </>
  );
}

Tasarım sistemlerinde button, input gibi düşük seviyeli bileşenlerin ref’leri DOM elemanlarına iletmeleri yaygın bir modeldir. Öte yandan formlar, listeler veya sayfa bölümleri gibi üst düzey bileşenler DOM yapısına yanlışlıkla eklenen bağımlılıkları önlemek için genellikle DOM elemanlarını göstermez.

Derinlemesine İnceleme

İmperatif bir işlem tanımı ile API’nin bir alt kümesini açığa çıkarma

Yukarıdaki örnekte MyInput orijinal DOM input elemanını ortaya çıkarır. Bu, üst bileşenin üzerinde focus()’u aramasına izin verir. Ancak bu, üst bileşenin başka bir şey yapmasına da izin verir örneğin CSS stillerini değiştirmek. Nadir durumlarda açığa çıkan işlevselliği kısıtlamak isteyebilirsiniz. Bunu useImperativeHandle ile yapabilirsiniz:

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Sadece focus'u ortaya çıkarın
    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>
    </>
  );
}

Burada MyInput içindeki realInputRef asıl input DOM elemanını tutar. Bununla birlikte useImperativeHandle, React’e üst bileşene bir ref değeri olarak kendi özel nesnenizi sağlamasını söyler. Dolayısıyla Form bileşeni içindeki inputRef.current sadece focus metoduna sahip olacaktır. Bu durumda “handle” ref’i DOM elemanı değildir ancak useImperativeHandle çağrısı içinde oluşturduğunuz özel nesnedir.

React refleri ne zaman ekler?

React’te her güncelleme iki aşamaya ayrılır:

  • React render etme esnasında ekranda ne olması gerektiğini anlamak için bileşenlerinizi çağırır.
  • React commit esnasında değişiklikleri DOM’a uygular.

Genel olarak render etme esnasında ref’lere erişmek istemezsiniz. Bu, DOM elemanlarını tutan ref’ler için de geçerlidir. İlk render esnasında DOM elemanları henüz oluşturulmadığında ref.current, null olacaktır. Güncellemelerin render edilmesi esnasında DOM elemanları henüz güncellenmedi. Bu yüzden onları okumak için çok erken.

React commit esnasında ref.current ayarını yapar. React DOM’u güncellemeden önce etkilenen ref.current değerlerini null olarak ayarlar. DOM’u güncelledikten sonra React hemen ilgili DOM elemanını ayarlar.

Ref’lere genellikle olay yöneticisinden erişirsiniz. Ref ile bir şey yapmak istiyorsunuz ancak bunu yapmak için belirli bir olay yoksa bir Effect’e ihtiyacınız olabilir. Sonraki sayfalarda bundan bahsedeceğiz.

Derinlemesine İnceleme

State güncellemelerini flushSync ile senkronize bir şekilde temizleme

Yeni bir yapılacak iş ekleyen ve ekranı listenin son alt ögesine kadar kaydıran bir kod düşünün. Her zaman son eklenenden hemen önceki yapılacak işe nasıl kaydırıldığına dikkat edin:

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)
  });
}

Sorun şu iki satırda:

setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

React’te state güncellemeleri sıraya alınır. Genellikle istediğiniz budur. Ancak burada bir soruna neden olur çünkü setTodos DOM’u hemen güncellemez. Bu yüzden listeyi son elemanına doğru kaydırdığınızda yapılacaklar henüz eklenmemiştir. Bu nedenle kaydırma her zaman bir eleman kadar geride kalır.

Bu sorunu gidermek için React’i DOM’u eşzamanlı olarak güncellemeye (“flush”) zorlayabilirsiniz. Bunu yapmak için flushSync’i react-dom’dan içeri aktarın ve state güncellemesini flushSync’un içinde çağırın:

flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

Bu yöntem React’e flushSync’e yazılmış kod çalıştırıldıktan hemen sonra DOM’u eşzamanlı olarak güncellemesini söyler. Sonuç olarak son elediğiniz yapılacaklar kaydırma yapmaya çalıştığınız zaman zaten DOM’da olacaktır:

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)
  });
}

Ref’ler ile DOM manipülasyonu için en iyi uygulamalar

Ref’ler kaçış kapısıdır. Bunu sadece “React’in dışına çıkmanız” gerektiğinde kullanmalısınız. Bunun yaygın örnekleri arasında focus yönetimi, kaydırma konumu veya React’in göstermediği tarayıcı API’lerini çağırmak yer alır.

Focus ve kaydırma gibi işlemlere bağlı kalırsanız herhangi bir sorunla karşılaşmazsınız. Ancak DOM’u manuel olarak değiştirmeye çalışırsanız React’in yaptığı değişikliklerle çakışma riskiyle karşı karşıya kalabilirsiniz.

Bu sorunu göstermek için aşağıdaki örnekte karşılama mesajı ve iki buton yer almaktadır. İlk buton genellikle React’te yaptığınız koşullu render etme ve state kullanarak değerini değiştirmedir. İkinci buton React’in kontrolü dışındaki DOM’dan zorla kaldırmak için remove() DOM API’sini kullanır.

Birkaç kez “Toggle with setState”e tıklamayı deneyin. Mesaj kaybolmalı ve tekrar görünmelidir. Ardından “Remove from the DOM”a tıklayın. Bu onu zorla kaldıracaktır. Son olarak “Toggle with setState”e tıklayın:

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>
  );
}

DOM elemanını manuel olarak kaldırdıktan sonra tekrar göstermek için setState’i kullanmaya çalışmak tutarsızlığa neden olur. Bunun nedeni DOM’u değiştirmiş olmanız ve React’in bunu doğru bir şekilde yönetmeye nasıl devam edeceğini bilmemesidir.

React tarafından yönetilen DOM elemanlarını değiştirmekten kaçının. React tarafından yönetilen elemanlarda değişiklik yapmak, alt elemanlar eklemek veya elemanları kaldırmak tutarsız görsel sonuçlara veya yukarıdaki gibi tutarsızlıklara neden olabilir.

Ancak bu hiç yapamayacağınız anlamına gelmez. Dikkat gerektirir. React’in güncellemek için bir nedeni olmayan DOM bölümlerini güvenle değiştirebilirsiniz. Örneğin JSX’te bazı <div> elemanları her zaman boşsa React’in alt listesine dokunmak için bir nedeni olmayacaktır. Bu nedenle elemanları buraya manuel olarak eklemek veya kaldırmak güvenlidir.

Özet

  • Ref’ler genel bir kavramdır ancak çoğu zaman bunları DOM elemanlarını tutmak için kullanırsınız.
  • React’e <div ref={myRef}> elemanını geçerek myRef.current’a bir DOM elemanı koymasını söylersiniz.
  • Genellikle DOM elemanlarına odaklama, kaydırma veya ölçme gibi zararsız işlevler için ref’leri kullanırsınız.
  • Bir bileşen varsayılan olarak DOM elemanlarını göstermez. forwardRef kullanarak ve ikinci ref parametresini belirli bir elemana geçirerek bir DOM elemanını göstermeyi seçebilirsiniz.
  • React tarafından yönetilen DOM elemanlarını değiştirmekten kaçının.
  • React tarafından yönetilen DOM elemanlarını değiştirmek isterseniz React’in güncellemek için bir nedeni olmayan kısımlarını değiştirin.

Problem 1 / 4:
Videoyu oynat ve duraklat

Bu örnekte buton, yürütme ve duraklatma işlemi arasında geçiş yapmak için state değişkenini değiştirir. Ancak videoyu gerçekten oynatmak ve duraklatmak için state geçişi yeterli değildir. Ayrıca <video> için DOM elemanında play() ve pause() elemanlarını çağırmanız gerekir. Buna bir ref ekleyin ve butonun çalışmasını sağlayın.

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>
    </>
  )
}

Ekstra bir zorluk için kullanıcı videoyu sağ tıklatıp yerleşik tarayıcı medya kontrollerini kullanarak oynatsa bile “Play” düğmesini videonun oynatılıp oynatılmadığıyla ilgili senkronize halde tutun. Bunu yapmak için videoda onPlay ve onPause olayını dinlemek isteyebilirsiniz.