Author: Luís Freitas

Date: 2021-05-28

Become a React dataflow emperor with forwardRefs and imperative handles

Make it make sense! React states need not be “trapped” in a component and its children. Instead, picture this:

  • You have a React component with some advanced logic and context that outputs some interesting results;

  • Those results are part of a component’s state and you’d like to leverage them somewhere else in your application.

Let us delve into how we can make an element be truly “transformative” to those that know about it’s reference.

What we will cover

  1. Some context;

  2. A primer on forwardRef;

  3. Introducing imperative handles;

  4. An abstract example;

  5. Closing thoughts.

Context

Sometime last week I was working on a feature for this awesome project I’ve been a part of, where there was a sizeable element tree with a shared state to match (picture state reducer definitions with hundreds of lines). This state tree had a varying number of inputs that would affect it, calling dispatch whenever text was changed — rerender galore!

The added challenge here is the fact that it is not usually a great idea to make whatever it is you’re working on go through a major refactor when in the middle of introducing a new feature.

The shared mindset was:

more optimizing the new things we introduce and less reinventing the wheel

which proved to be a useful mindset and allowed us to retrofit existing code to match the new patterns we discovered.

Reinventing the wheel comic

This new feature introduces several instances of a complex text editor to an already highly dynamic page so my code reusing patterns from the good and not-so-old C++ days came back to me and I thought:

If only there was a way to make those fancy editor components reusable and take advantage of the fact that they manage their own state internally, making only the data we’re interested in available at a given time, e.g. in event handling callbacks...

Enter React’s forwardRef

The usage of refs in functional components typically has a close association with directly manipulating the DOM tree — something that, due to the nature of React, you won’t always know what it looks like. At times, this can be more confusing than it is useful.

Refs can be used to observe and manipulate content and how it lives on the page before your eyes, so why not do it the way you want?

The useImperativeHandle hook

Let us go back to the scenario at the beginning of this article. Knowing that our text editor component is ultimately an HTML input, we could have used plain old refs, accessed event data, and called it a day… Or could we?

If we are given these awesome internal document/state representations, (our wheels, if you will) baked into the editor’s functionality, why would we want to do that? With useImperativeHandle we were able to take control of the component’s internals to the next level.

The way useImperativeHandle works is by overriding what the React element’s ref points to, where you can express dependencies in an array passed as argument, as if dealing with any other type of React hook because you are!

According to React’s own hooks API reference, this hook’s arguments can be described as:

useImperativeHandle(ref, createHandle, [deps])

Where ref is the component’s (forwarded) reference and [deps] an (optional) arbitrary dependency array. The real magic happens in createHandle’s function definition, setting the ref’s current property to whatever you want it to be.

pickard

The text editor we are working with offers the notion of context where an internal document representation (the state data structure), helper functions and a transaction manager are available. Given that the most recent state of the document being manipulated is required, it’s a simple matter of, in tandem with this new hook we are learning about, overriding the component’s onBlur event (or any other event that makes sense in your use case, for that matter) to make it carry the data we are interested in.

Defining createHandle in the hook above as

() => editorContext

we make the internal editor context data available to whoever knows about (or owns) the ref passed into the editor.

Now, let us express onBlur as:

onBlur={() => {
  if (props.onBlur) props.onBlur(ref)
}}

Notice the ref being passed in as an argument, effectively making the context data, as per createHandle’s definition, be made available to the callee.

An abstract example

Recalling our need to avoid re-renders, let us build a simplified pattern that would allow us to access editor context from the parent component and dispatch a manipulated result to some state reducer:

import MyFancyEditor from ‘./MyFancyEditor’
import { useSomeReducer } from ‘./SomeContextProvider’const ParentComponent = () => {  const { dispatch } = useSomeReducer();
  const editorRef = React.createRef();return(
  // Some other components
  <MyFancyEditor
    ref={editorRef}
    onBlur={({current}) => dispatch({
        // Some other values
        document: current.helpers.getJSON(current.state),
      })
    }
  />
)}

It is important to see editorRef being created, passed into the editor and how this ties into its own onBlur definition, as well as the destructuring of the current prop in the incoming reference object to access the context’s data.

Closing thoughts

Refs and imperative handles can have the level of complexity we’d like to provide them with, and that’s the beauty of using them!

With forwardRef and useImperativeHandle you can implicitly adopt the delegation pattern for React components.

Where the ref passed to these is viewed as the delegate to the component itself, since — depending on how createHandle is defined — it retains the original component’s context or at least the part of it we deem relevant.

Similar to anything in JavaScript, the returned context should have well-defined properties that can be modified or at least observed without completely breaking the component in the process — granted you do not forget this, move on do ref great things!

After-thoughts

« back to blog list