I've been boxed into a rather uncomfortable situation in React where a parent element has to manage the refs of many children in a large (read: expensive-to-render) component tree that changes over time. The children whose refs I need also changes, and I need to hook into some part of the lifecycle to ensure that all the children I need to track have a corresponding ref to hook into.
For context: the component tree is expensive because it's several thousand elements that render into a syntax-highlighted source file, and I use the child refs for positioning tooltips and scrolling. I'm trying to avoid re-rendering the component tree on these superficial visual changes, and only do so when the source file content changes. Importantly, maintaining the dictionary of refs shouldn't introduce new renders.
Minimally, our component looks something like this:
type TProps = {
children_to_track: ChildProps[]
}
class Parent extends React.Component {
constructor(props) {
super(props);
// this.state = {};
}
render() {
return <div>
{/* lots of static children */}
{ this.props.children_to_track.map(childprops =>
<Child
{...childprops}
key={childprops.id}
ref={/* NEED REF */} />
) }
</div>
}
}
The few solutions that have come to mind are:
In
shouldComponentUpdate
(andcomponentDidMount
for the initial case), copy the prop that specifies the children into state and pair them with a React ref, returning false on this first pass. When this state change hitsshouldComponentUpdate
again, return true and re-render:type TState = { children_to_track: Array<[ChildProps, React.RefObject<any>]> }; shouldComponentRender(pprops: TProps, pstate: TState) { if(pprops.children_to_track !== this.props.children_to_track) { this.setState({ children_to_track: this.props.children_to_track.map(c => [c, React.createRef()]) }); return false; } else { return true; } }
Use
UNSAFE_componentWillUpdate
to stash a dictionary of refs directly into the object properties just before render, and assign the refs from this dictionary:UNSAFE_componentWillRender() { this.child_refs = this.props.children_to_track.map(_ => React.createRef()); }
Similar to 2. but even dirtier, create the refs in
render
directly and stash them to the object's dictionary there.Use callback refs. This is not ideal because these aren't called if the component remains the same between renders. As a result, I can no longer wholesale-refresh the ref dictionary on each prop change, but instead have to diff them and keep the refs that won't change. Also tightly couples to the reconciliation semantics, which I don't like.
getSnapshotBeforeUpdate
and componentDidUpdate
are both called after render()
and are too late to prevent a re-render if they are the ones to stash the new refs. I'll only resort to them if all the other single-render solutions end up being too heinous.
I'm leaning towards 1), but setting state in shouldComponentUpdate
still feels awful. Any suggestions?