Docs / rescript-react / Refs and the DOM
Edit

You are currently looking at the v0.12.0 docs, which are still a work in progress. If you miss anything, you may find it in the older v0.11.0 docs here.

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:

RES
type 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 state that should not trigger any re-render.

  • 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.

RES
let 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 of React.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:

ReScriptJS Output
// CustomTextInput.res

@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 a Js.Nullable.null

  • We register our textInput ref in our <input> element with ReactDOM.Ref.domRef(textInput)

  • In our focusInput function, we need to first verify that our DOM element is set, and then use the focus 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:

ReScriptJS Output
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.

ReScriptJS Output
// CustomTextInput.res

@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().

ReScriptJS Output
// Parent.res

@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.