Refs and the DOM
Refs provide a way to access DOM nodes or React elements created within your make
component function.
In the typical React dataflow, props are the only way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are a few cases where you need to imperatively modify a child outside of the typical dataflow. The child to be modified could be an React.element
, or it could be a Dom.element
. For both of these cases, React provides an escape hatch.
A React.ref
is defined like this:
REStype t<'value> = { mutable current: 'value }
Note that the
Ref.ref
should not to be confused with the builtin ref type, the language feature that enables mutation.
When to use Refs
There are a few good use cases for refs:
Managing focus, text selection, or media playback.
Triggering imperative animations.
Integrating with third-party DOM libraries.
Avoid using refs for anything that can be done declaratively.
Creating Refs
A React ref is represented as a React.ref('value)
type, a container managing a mutable value of type 'value
. You can create this kind of ref with the React.useRef hook:
RES@react.component
let make = () => {
let clicks = React.useRef(0);
let onClick = (_) => {
clicks.current = clicks.current + 1;
};
<div onClick>
{Belt.Int.toString(clicks.current)->React.string}
</div>
}
The example above defines a binding clicks
of type React.ref(int)
. Note how changing the value clicks.current
doesn't trigger any re-rendering of the component.
Accessing Refs
When a ref is passed to an element during render, a reference to the node becomes accessible at the current attribute of the ref.
RESlet value = myRef.current
The value of the ref differs depending on the type of the node:
When the ref attribute is used on an HTML element, the ref passed via
ReactDOM.Ref.domRef
receives the underlying DOM element as its current property (type ofReact.ref<Js.Nullable.t<Dom.element>>
)In case of interop, when the ref attribute is used on a custom class component (based on JS classes), the ref object receives the mounted instance of the component as its current (not discussed in this document).
You may not use the ref attribute on component functions because they don’t have instances (we don't expose JS classes in ReScript).
Here are some examples:
Adding a Ref to a DOM Element
This code uses a React.ref
to store a reference to an input
DOM node to put focus on a text field when a button was clicked:
// CustomTextInput.res
@bs.send external focus: Dom.element => unit = "focus"
@react.component
let make = () => {
let textInput = React.useRef(Js.Nullable.null)
let focusInput = () =>
switch textInput.current->Js.Nullable.toOption {
| Some(dom) => dom->focus
| None => ()
}
let onClick = _ => focusInput()
<div>
<input type_="text" ref={ReactDOM.Ref.domRef(textInput)} />
<input type_="button" value="Focus the text input" onClick />
</div>
}
A few things happened here, so let's break them down:
We initialize our
textInput
ref as aJs.Nullable.null
We register our
textInput
ref in our<input>
element withReactDOM.Ref.domRef(textInput)
In our
focusInput
function, we need to first verify that our DOM element is set, and then use thefocus
binding to set the focus
React will assign the current
field with the DOM element when the component mounts, and assign it back to null when it unmounts.
Refs and Component Functions
In React, you can't pass a ref
attribute to a component function:
module MyComp = {
@react.component
let make = (~ref) => <input />
}
@react.component
let make = () => {
let textInput = React.useRef(Js.Nullable.null)
// This will **not** work
<MyComp ref={ReactDOM.Ref.domRef(textInput)} />
}
The snippet above will not compile and output an error that looks like this: "Ref cannot be passed as a normal prop. Please use forwardRef API instead."
.
As the error message implies, If you want to allow people to take a ref to your component function, you can use ref forwarding (possibly in conjunction with useImperativeHandle) instead.
Exposing DOM Refs to Parent Components
In rare cases, you might want to have access to a child’s DOM node from a parent component. This is generally not recommended because it breaks component encapsulation, but it can occasionally be useful for triggering focus or measuring the size or position of a child DOM node.
we recommend to use ref forwarding for these cases. Ref forwarding lets components opt into exposing any child component’s ref as their own. You can find a detailed example of how to expose a child’s DOM node to a parent component in the ref forwarding documentation.
Callback Refs
React also supports another way to set refs called “callback refs” (React.Ref.callbackDomRef
), which gives more fine-grain control over when refs are set and unset.
Instead of passing a ref value created by React.useRef()
, you can pass in a callback function. The function receives the target Dom.element
as its argument, which can be stored and accessed elsewhere.
Note: Usually we'd use React.Ref.domRef()
to pass a ref value, but for callback refs, we use React.Ref.callbackDomRef()
instead.
The example below implements a common pattern: using the ref callback to store a reference to a DOM node in an instance property.
// CustomTextInput.re
@bs.send external focus: Dom.element => unit = "focus"
@react.component
let make = () => {
let textInput = React.useRef(Js.Nullable.null)
let setTextInputRef = element => {
textInput.current = element;
}
let focusTextInput = _ => {
textInput.current
->Js.Nullable.toOption
->Belt.Option.forEach(input => input->focus)
}
<div>
<input type_="text" ref={ReactDOM.Ref.callbackDomRef(setTextInputRef)} />
<input
type_="button" value="Focus the text input" onClick={focusTextInput}
/>
</div>
}
React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts.
You can pass callback refs between components like you can with object refs that were created with React.useRef()
.
// Parent.res
@bs.send external focus: Dom.element => unit = "focus"
module CustomTextInput = {
@react.component
let make = (~setInputRef) => {
<div>
<input type_="text" ref={ReactDOM.Ref.callbackDomRef(setInputRef)} />
</div>
}
}
@react.component
let make = () => {
let textInput = React.useRef(Js.Nullable.null)
let setInputRef = element => { textInput.current = element}
<CustomTextInput setInputRef/>
}
In the example above, Parent
passes its ref callback as an setInputRef
prop to the CustomTextInput
, and the CustomTextInput
passes the same function as a special ref attribute to the <input>
. As a result, the textInput
ref in Parent will be set to the DOM node corresponding to the <input>
element in the CustomTextInput
.