Ref ব্যবহার করে DOM ম্যানিপুলেশন
React স্বয়ংক্রিয়ভাবে DOM আপডেট করে যেন এটি আপনার রেন্ডার আউটপুটের সাথে মিলে যায়, তাই আপনার components দ্বারা প্রায় সময়ই এটি পরিবর্তন করার প্রয়োজন হবে না। তবে, মাঝে মাঝে আপনার React দ্বারা পরিচালিত DOM elements এর প্রয়োজন হতে পারে—উদাহরণস্বরূপ, একটি node কে focus করতে, scroll করতে, অথবা এর আকার এবং অবস্থান পরিমাপ করতে। React এ এই ধরণের কাজ করার জন্য কোনো বিল্ট-ইন উপায় নেই, তাই আপনার DOM node এর ref প্রয়োজন হবে।
আপনি যা শিখবেন
- কীভাবে React এর পরিচালিত একটি DOM নোড
ref
এট্রিবিউট ব্যবহার করে অ্যাক্সেস করবেন - কীভাবে
ref
JSX এট্রিবিউটuseRef
হুকের সাথে সম্পর্কিত। - কীভাবে অন্য একটি কম্পোনেন্টের DOM নোড অ্যাক্সেস করবেন
- কোন কোন ক্ষেত্রে React এর পরিচালিত একটি DOM নোড পরিবর্তন করা নিরাপদ
একটা নোডকে ref পর্যন্ত নিয়ে যাওয়া
React এর পরিচালিত একটি DOM নোড অ্যাক্সেস করতে, প্রথমে, useRef
হুকটি ইমপোর্ট করুনঃ
import { useRef } from 'react';
এর পর, একে ব্যবহার করে আপনার কম্পোনেন্টের মধ্যে একটি ref ডিক্লেয়ার করেনঃ
const myRef = useRef(null);
সবশেষে, DOM নোডে একে ref
এট্রিবিউট হিসেবে পাস করে দিনঃ
<div ref={myRef}>
useRef
হুক একটি অবজেক্ট রিটার্ন করে যার একটি মাত্র প্রপার্টি থাকে current
নামে। প্রাথমিকভাবে, myRef.current
হবে null
। যখন React এই <div>
এর জন্য একটি DOM node তৈরি করে, React এই নোডের একটি রেফারেন্স myRef.current
-এ রাখবে। তারপর আপনি আপনার event handlers থেকে এই DOM node এ অ্যাক্সেস করতে পারেন এবং এর উপর defined বিল্ট-ইন browser APIs ব্যবহার করতে পারেন।
// আপনি যেকোন ব্রাউজার API ব্যবহার করতে পারেন। উদাহরণস্বরূপঃ
myRef.current.scrollIntoView();
উদাহরণঃ একটা টেক্সট ইনপুটে ফোকাস করা
এই উদাহরণে, বাটনে ক্লিক করলে ইনপুট ফোকাস হবেঃ
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> </> ); }
এটা করার জন্যঃ
useRef
হুক ব্যবহার করেinputRef
ডিক্লেয়ার করুন।<input ref={inputRef}>
হিসেবে একে পাস করে দিন। এটা React কে বলবে যেন এই<input>
এর DOM নোডinputRef.current
এর মধ্যে রাখা হয়।handleClick
ফাংশনে,inputRef.current
থেকে ইনপুট DOM নোড রিড করুন এবংinputRef.current.focus()
দিয়ে এর উপরfocus()
কল করুন।handleClick
ইভেন্ট হ্যান্ডলারটিonClick
এর সাহায্যে<button>
এ পাঠিয়ে দিন।
যদিও DOM ম্যানিপুলেশন এর জন্যই refs এর ব্যবহার সবচেয়ে বেশি হয়, useRef
হুক React এর বাইরে অন্যান্য জিনিস সংরক্ষণ করার জন্য ব্যবহৃত হতে পারে, যেমন টাইমার ID। State এর মতোই, রেন্ডারে মধ্যবর্তী সময়ে ref ঠিকঠাক থাকে। Refs হল state variables এর মত যেগুলি সেট করার সময় re-render ট্রিগার হয় না। রেফস সম্বন্ধে পড়ুন ref এর সাহায্যে ভ্যালু রেফারেন্সিং অংশে।
উদাহরণঃ কোন এলিমেন্ট পর্যন্ত স্ক্রল করা
আপনি একটি কম্পোনেন্টে একাধিক ref রাখতে পারেন। এই উদাহরণে, তিনটি চিত্রের একটি ক্যারাসেল আছে। প্রতিটি বাটন ব্রাউজারে ওই ছবির সাথে সম্পর্কিত DOM নোডে scrollIntoView()
মেথড কল করে ছবিটিকে মাঝামাঝি আনে।
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> </> ); }
গভীরভাবে জানুন
উপরের উদাহরণগুলিতে, ref এর একটি পূর্বনির্ধারিত সংখ্যা রয়েছে। যদিও, মাঝে মাঝে আপনার তালিকার প্রতিটি আইটেমের জন্য একটি ref প্রয়োজন হতে পারে, এবং আপনি জানবেন না আপনার কতগুলো থাকবে। এরকম কিছু কাজ করবে নাঃ
<ul>
{items.map((item) => {
// কাজ করে না!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
এর কারণ হুক শুধুমাত্র আপনার কম্পোনেন্টের শীর্ষ-স্তরে ডাকতে হবে। আপনি একটি লুপে, একটি কন্ডিশনে, বা একটি map()
কলের ভিতরে useRef
ডাকতে পারবেন না।
একে এডিয়ে যাবার একটি সম্ভাব্য উপায় হল প্যারেন্ট এলিমেন্টে একটা মাত্র ref নিয়ে যাওয়া এবং তারপরে DOM ম্যানিপুলেশন পদ্ধতিগুলি যেমন querySelectorAll
ব্যবহার করে এটি থেকে individual child node “খুঁজে” বের করা। যদিও, এটি অত্যন্ত নাজুক এবং যদি আপনার DOM কাঠামো পরিবর্তন হয় তবে এটি ভেঙে যেতে পারে।
অন্য একটি সমাধান হলো ref
এট্রিবিউটে একটি ফাংশন পাস করা। এটি ref
callback. নামে পরিচিত। React যখন রেফ সেট করার সময় হবে তখন এটি আপনার ref callback কে DOM নোড দিয়ে কল করবে, এবং যখন এটি সাফ করার সময় হবে তখন null
দিয়ে ডাকবে। এটি আপনাকে আপনার নিজের একটি array বা একটি Map বজায় রাখতে দেয়, এবং এর মাধ্যমে আপনি তার ইনডেক্স বা কোন ধরণের ID দ্বারা যেকোনো ref অ্যাক্সেস করতে পারেন।
এই উদাহরণটি দেখায় যে আপনি এই পদ্ধতিটি কীভাবে ব্যবহার করে একটি দীর্ঘ তালিকায় যেকোনো নোডে স্ক্রল করতে পারেনঃ
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) { // Initialize the Map on first usage. 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 }); }
In this example, itemsRef
doesn’t hold a single DOM node. Instead, it holds a Map from item ID to a DOM node. (Refs can hold any values!) The ref
callback on every list item takes care to update the 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);
}
}}
>
This lets you read individual DOM nodes from the Map later.
Accessing another component’s DOM nodes
আপনি যখন এমন একটা বিল্ট-ইন কম্পোনেন্টে রেফ বসান যা <input />
এর মত একটি ব্রাউজার এলিমেন্ট আউটপুট হিসেবে দেয়, React সেই ref এর current
হিসেবে প্রপার্টি সম্পর্কিত DOM নোড (যেমন ব্রাউজারের প্রকৃত <input />
) সেট করে দেবে।
তবে, আপনি যদি আপনার নিজের কম্পোনেন্টে একটা ref বসাতে চান, যেমন <MyInput />
, তাহলে স্বাভাবিকভাবে আপনি null
পাবেন। এখানে বিষয়টা দেখায় এমন একটি উদাহরণ দেখানো হল। খেয়াল করুন বাটনে ক্লিক করলে ইনপুটে ফোকাস হয় না।
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> </> ); }
আপনি যেন ঝামেলাটা খেয়াল করেন, সেজন্য React কনসোলে একটি এরর দেখিয়ে দেয়ঃ
এটি ঘটে কারণ স্বাভাবিকভাবে React কম্পোনেন্টকে অন্যান্য কম্পোনেন্টের DOM নোড অ্যাক্সেস করতে দেয় না। তার নিজের childrenদের জন্যও নয়! এটি ইচ্ছাকৃত। Ref এক ধরণের escape hatch যা খুব কম ব্যবহার করা উচিত। ম্যানুয়ালি অন্য কম্পোনেন্টের DOM নোড পরিবর্তন করা আপনার কোডকে আরও নাজুক বানিয়ে ফেলে।
Instead, components that want to expose their DOM nodes have to opt in to that behavior. A component can specify that it “forwards” its ref to one of its children. Here’s how MyInput
can use the forwardRef
API:
এর পরিবর্তে, যে কম্পোনেন্টগুলি তাদের DOM নোড উন্মুক্ত করতে চায় তাদেরকে এই আচরণ আয়ত্ব করে নিতে হবে। একটি কম্পোনেন্ট নির্দিষ্ট করতে পারে যে এটি তার ref তার একটি সন্তানের কাছে “ফরোয়ার্ড” করে। এটা কিভাবে MyInput forwardRef API ব্যবহার করতে পারে তা দেখানো হলো:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
এটা কাজ করে এই ভাবেঃ
<MyInput ref={inputRef} />
React কে বলে corresponding DOM নোডinputRef.current
এর মধ্যে রাখতে। কিন্তু, এই সিদ্ধান্তটাMyInput
কম্পোনেন্টের উপর নির্ভর করে যে সে এটা করবে কি না—স্বাভাবিকভাবে সে এটা করে না।MyInput
কম্পোনেন্টটাforwardRef
ব্যবহার করে ডিক্লেয়ার করা হয়। এটা উপরেরinputRef
কে দ্বিতীয়ref
আর্গুমেন্ট হিসেবে নেওয়ার সিদ্ধান্ত নেয়, যাprops
এর পরে ডিক্লেয়ার করা হয়।MyInput
যেইref
টা পায় সেটা নিজেই এর ভিতরকার<input>
এ পাস করে দেয়।
এখন বাটন ক্লিক করে ইনপুট ফোকাস হচ্ছে ঠিকঠাকভাবেঃ
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> </> ); }
In design systems, it is a common pattern for low-level components like buttons, inputs, and so on, to forward their refs to their DOM nodes. On the other hand, high-level components like forms, lists, or page sections usually won’t expose their DOM nodes to avoid accidental dependencies on the DOM structure.
গভীরভাবে জানুন
In the above example, MyInput
exposes the original DOM input element. This lets the parent component call focus()
on it. However, this also lets the parent component do something else—for example, change its CSS styles. In uncommon cases, you may want to restrict the exposed functionality. You can do that with 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> </> ); }
Here, realInputRef
inside MyInput
holds the actual input DOM node. However, useImperativeHandle
instructs React to provide your own special object as the value of a ref to the parent component. So inputRef.current
inside the Form
component will only have the focus
method. In this case, the ref “handle” is not the DOM node, but the custom object you create inside useImperativeHandle
call.
When React attaches the refs
In React, every update is split in two phases:
- During render, React calls your components to figure out what should be on the screen.
- During commit, React applies changes to the DOM.
In general, you don’t want to access refs during rendering. That goes for refs holding DOM nodes as well. During the first render, the DOM nodes have not yet been created, so ref.current
will be null
. And during the rendering of updates, the DOM nodes haven’t been updated yet. So it’s too early to read them.
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.
গভীরভাবে জানুন
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.