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:
useRef
Hook’u ileinputRef
’i bildirin.<input ref={inputRef}>
olarak geçin. React’e bu<input>
’ta DOM elemanının içineinputRef.current
’i koymasını söyler.handleClick
fonksiyonundainputRef.current
’tan input DOM elemanını okuyun vefocus()
ileinputRef.current.focus()
ögesini çağırın.handleClick
olay yöneticisinionClick
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
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:
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:
<MyInput ref={inputRef} />
React’e, karşılık gelen DOM elemanınıinputRef.current
’a koymasını söyler. Ancak bunu seçmekMyInput
bileşenine bağlıdır. Varsayılan olarak bunu yapmaz.MyInput
bileşeniforwardRef
kullanılarak tanımlanır.props
’tan sonra bildirilen ikinciref
, parametre olarak yukarıdaninputRef
’i almayı seçer.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
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
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çerekmyRef.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 ikinciref
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.