0

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:

  1. In shouldComponentUpdate (and componentDidMount 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 hits shouldComponentUpdate 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;
     }
    }
    
  2. 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());
    }
    
  3. Similar to 2. but even dirtier, create the refs in render directly and stash them to the object's dictionary there.

  4. 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?

concat
  • 3,107
  • 16
  • 30

1 Answers1

0

It hadn't occurred to me initially to just wrap the component tree in a PureComponent that is dependent on the state copy of the children_to_track, so I can re-render freely in the parent:

const Wrapper = React.memo(props => <div>
  {/* lots of static children */}
  { props.children_to_track.map(([ch, ref]) =>
    <Child {...ch}
      key={ch.id}
      ref={ref} />
  ) }
</div>);
class Parent extends React.Component<...> {
  // ...
  componentDidUpdate(pprops: TProps) {
    if(pprops.children_to_track !== this.props.children_to_track)
      this.setState({
        children_to_track: this.props.children_to_track.map(ch => [ch, React.createRef()])
      })
  }
  render() {
    return <Wrapper children_to_track={this.state.children_to_track} />
  }
}
concat
  • 3,107
  • 16
  • 30