refs: somewhat similar to state, but with some differences:
first use case:
storing values / object references that don't influence the rendering of a component:
second use case:
storing the most up-to-date version of a state
In function components, refs can be created / accessed via useRef
Values are stored in the reference's .current
property
an example of both use cases:
function StopWatch() {
const [time, setTime] = useState(0);
const timeRef = useRef(time);
const intervalId = useRef<number>(null);
function start() {
setTime(0);
timeRef.current = 0;
intervalId.current = setInterval(() => {
timeRef.current++;
setTime(timeRef.current);
}, 1000);
}
function stop() {
clearInterval(intervalId.current);
}
return <div>...</div>;
}
Note: often, there can be implementations that avoid refs:
function StopWatch() {
const [time, setTime] = useState(0);
const [running, setRunning] = useState(false);
useEffect(() => {
if (running) {
const intervalId = setInterval(() => {
setTime((time) => time + 1);
}, 1000);
return () => clearInterval(intervalId);
}
}, [running]);
return <div>...</div>;
}
Just like key, the ref property has a special meaning in JSX - enabling direct access to rendered DOM elements
use cases:
Using the ref property together with the useRef hook:
function RefDemo() {
const myRef = useRef<HTMLInputElement>(null);
return (
<input
ref={myRef}
onChange={() => {
console.log(myRef.current.value);
}}
/>
);
}
managing focus, text selection, or media playback
some changes cannot be expressed declaratively (via state); they require direct access to a DOM element
example: there are properties like .value
for changing a value or .className
for changing classes, but there is no property for managing focus
Managing focus:
function App() {
const inputEl = useRef<HTMLInputElement>(null);
useEffect(() => {
inputEl.current?.focus();
}, []);
return <input ref={inputEl} />;
};
managing media playback:
// https://codesandbox.io/s/media-playback-x3ci4
function Video() {
const [playing, setPlaying] = useState(false);
const videoEl = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (playing) {
videoEl.current?.play();
} else {
videoEl.current?.pause();
}
}, [playing]);
return (
<div>
<video
ref={videoEl}
width="250"
src={
'https://interactive-examples.mdn.mozilla.net/' +
'media/cc0-videos/flower.mp4'
}
/>
<button onClick={() => setPlaying(!playing)}>
{playing ? 'pause' : 'play'}
</button>
</div>
);
}
integrating with third-party DOM libraries
Third-party libraries may require a DOM element being passed in
Example: Google Maps takes an element where it will paint the map
Many third-party libraries have wrappers for React where refs are not needed
integrating Google Maps via a ref:
function Map() {
const mapRef = useRef<HTMLElement>();
useEffect(() => {
new google.maps.Map(mapRef.current);
}, []);
return <div ref={mapRef} style={height: 400} />;
}
alternative way of managing inputs
using ref
instead of value
and onChange
can mean less code (but is discouraged by the React documentation)
Refs are used by react-hook-form to make form handling simpler and faster
Managing inputs: comparing useState
and useRef
:
const App = () => {
const [firstName, setFirstName] = useState('');
const lastNameInput = useRef<HTMLInputElement>(null);
return (
<div>
<input
value={firstName}
onChange={(event) =>
setFirstName(event.target.value)
}
/>
<input ref={lastNameInput} />
<button
onClick={() => {
console.log(firstName);
console.log(lastNameInput.current.value);
}}
>
log values
</button>
</div>
);
};
As we've seen we can pass a Ref object into the ref property
We can also pass in a callback function which will be called with the element as its parameter (react-hook-form uses this)
<input
ref={(element) => {
console.log(element);
console.log(element.value);
element.focus();
}}
/>
example: TextField
component in MUI
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
<TextField ref={containerRef} inputRef={inputRef} />
ref
: can be used to access the containing <div>
inputRef
: can be used to access the <input>
elementimplementation of a ref whose property name is not "ref":
type Props = {
inputRef: RefObject<HTMLInputElement>;
// ...
};
function TextField(props: Props) {
return (
<div>
...
<input ref={props.inputRef} />
</div>
);
}
implementation of a ref whose property name is "ref":
type Props = {
// ...
};
const TextField = forwardRef<HTMLDivElement, Props>(
(props, ref) => {
return (
<div ref={ref}>
...
<input />
</div>
);
}
);
combined implementation:
type Props = {
inputRef: RefObject<HTMLInputElement>;
// ...
};
const TextField = forwardRef<HTMLDivElement, Props>(
(props, ref) => {
return (
<div ref={ref}>
...
<input ref={props.inputRef} />
</div>
);
}
);