useRef( ) : DOM and beyond...
Shubham Sananse
Posted on April 28, 2021
This blog assumes that you know the React fundamentals and useState
hook.
Read my article on what is a state in an App? and useState hook (there's a TL;DR section, if you want to have a quick look ๐), you would need some of that state-related concept in this blog.
What is useRef
?
Ref is just a
{ current: initialValue }
object. It's nothing special. BothuseRef(initivalValue)
andcreateRef()
give you that. - Dan Abramov
function useRef(initialValue) {
// useRef works more like this
return React.useState({
current: initialValue
})[0]
}
You just created your own
useRef
. ๐คฏ๐คฏ๐คฏ
Why do we need useRef
?
useRef
actually serves two purposes,
- Provides a reference to the DOM elements
- Returns mutable value which persists across renders
But, what is this mutable and persistent value?
Persistent value is the kind of value that stays persistent between renders, that's what useState
returns, a persistent value (state
) and updater API (setState
) to update that state which causes a re-render for that component. for an application to update its View(UI) you need that setState API.
But what if you want to have a value that stays persistent and does not cause a re-render of a Component.๐ค
This is such a fundamental need that react provides a built-in API for it, That's what useRef is for.
useRef
// you can set any type of data as initialValue same as useState()
const objectWithCurrentProperty = React.useRef(initialValue)
const refOne = React.useRef() // returns {current : undefined}
const refTwo = React.useRef(1) // returns {current : 1}
const refThree = React.useRef([]) //returns {current : []}
useRef
takes the initial value as an argument for the returned value.
These return values would be persisted and you can also mutate them according to your need.
Accessing the DOM with useRef
There are some libraries (e.g. Three.js or Anime.js) that need access to the dom.
when we write jsx it gets converted into React.createElement. a <div>Hello World</div>
which we write as jsx gets converted into React.createElement("div", null, "Hello World")
so you don't have any direct access to the DOM nodes from your returned jsx.
So to get access to the DOM, you need to ask React to give you access to a particular DOM node when it renders your component. The way this happens is through a special prop called ref
.
function UploadButton({ handleUpload }) {
const inputRef = React.useRef();
const handleClick = () => inputRef.current.click();
return (
<>
<input type="file" hidden ref={inputRef} onInput={handleUpload} />
<button onClick={handleClick}>Upload</button>
</>
);
}
In this example, we are passing the ref
prop inputRef
to the hidden file input, and when we click on a button that uses inputRef.current
to get access to that DOM element of that input and on that element we are calling click
event.
some other cases would be like getting a value from an input, changing focus, or selecting text.
react-hook-form is a form library that encourages uncontrolled way of handling forms and
ref
prop to handle forms in react component, give it a try it's awesome.
Mutable Data Storage
Before we try to understand what this is and why do we need this? If you can, I would suggest you create a stopwatch component with stop and resume functionality.
.
.
.
Spoilers ahead....
.
.
.
// Here is a simple StopWatch component which updates time every 1 second
function StopWatch() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime((s) => s + 1);
}, 1000);
// clearInterval before unmounting component
return () => clearInterval(interval);
}, []);
return (<div>{time}</div>);
}
But now we need a button which will make the ticking of time stop and resume, for that we would add ticking state and update our useEffect.
function StopWatch() {
const [time, setTime] = useState(0);
const [ticking, setTicking] = useState(false);
useEffect(() => {
if (ticking) {
const interval = setInterval(() => {
setTime((ms) => ms + 1)
}, 1000);
return () => clearInterval(interval);
} else {
// ๐ค but we don't have access "interval" here
clearInterval(interval)
}
}, [ticking]);
return (
<div>
<div>{time}</div>
<button onClick={() => setTicking(c => !c)}>{ticking ? 'Pause' : 'Resume'}</button>
</div>
)
}
So where do we put our interval now? if you put this outside useEffect
on every render all local variables would reset and it would become undefined again
function StopWatch() {
...
// I ๐ would keep becoming undefined on every re-render ๐ข
let interval;
useEffect ...
}
So now we want something that stays persistent across renders and doesn't cause re-renders, and you guessed it right, we need useRef here.
function StopWatch() {
const [time, setTime] = useState(0)
const [ticking, setTicking] = useState(false)
// mutable and persistant
const interval = useRef()
useEffect(() => {
if (ticking) {
// `interval` would not reset when component re-renders
interval.current = setInterval(() => {
setTime((ms) => ms + 1)
}, 1000)
return () => clearInterval(interval.current)
} else {
// and now you have access to the interval
interval.current && clearInterval(interval.current)
}
}, [ticking])
return (
<div className="App">
<h1>{time}</h1>
<button onClick={() => setTicking(c => !c)}>
{time === 0 ? 'Start' : ticking ? 'Pause' : 'Resume'}
</button>
</div>
)
}
Where to go from here? from now you can go and read more about forwarding refs and useImperativeHandle. Thanks for reading.
Posted on April 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 18, 2024