Exposing slots in layout containers through shared refs in React

React refs are generally considered an anti-pattern as their usage typically encourages patterns which go against declarative compositon and top down flow of data.

This post explores a somewhat uncommon use case where refs can be used to expose layout slots in parent components to nested components.

The newly introduced React.createRef function makes it particularly convenient to create refs in parent components, and pass them down as props to other components.

While building business applications, I find it convenient to create reusable layout container components (eg. Header + Top Menu + Sidebar) and share them across different LOB applications.

While this is uncommon, I have occasionally encountered use cases which may benefit from a component being able to render sub-parts of itself “somewhere-else” in the layout structure, without the consumer of the component having to take care of rendering multiple components in different locations in the tree.

For example, let us say we are creating a DataTable component. When we click on a row, we open a drawer and render a form inside for editing the item represented by the row. Later, we decide that we would also want the flexibility to show this form outside the component in a sidebar exposed by a layout container, if available.

We could handle this by rendering the sidebar as a part of the DataTable component itself, but that is not suitable if the sidebar may contain other components too which are unrelated to our DataTable.

We could also expose this drawer as a separate component, and both our DataTable component as well as the RowDetailsForm component could be connected to a shared store, and rendered in independent locations in the component hierarchy, but this obviously requires more work on the part of consumer, and requires us to expose our internal state management concerns to outside world.

The third solution, which we discuss here, is to use shared refs.

The consumer component can create a ref and share it with both the layout container component and the presenter component.

Now, our container component can use this sidebarRef to designate a slot which can be populated by the nested presenter:

And our presenter component can use this shared sidebarRef as a Portal target. Portal here is the Portal component available from react-overlays package, which is essentially a declarative wrapper over ReactDOM.createPortal API.

In case you are wondering how this works during initial render, before the sidebarRef.current is available, the Portal component implementation can be illuminating:

Abbreviated source is below:

The render safely handles the scenario when the target container is not available yet, and in componentDidMount lifecycle hook, once the target is available, we set this._portalContainerNode and trigger a re-render.

This approach also makes it trivial to fallback to a local node if the consumer hasn’t supplied us with a sidebarRef.

Note that things like event bubbling will work identially in both scenarios even if the component is rendered out of the DOM hierarchy of parent. React’s synthetic event implementation is aware of portals and events are propagated through portal boundaries to parent components.