facebook/react

[React 19] `findDOMNode` is removed, with no suitable replacement for text nodes

smoores-dev opened this issue · 2 comments

Summary

In React 18 and below, the only way to obtain a reference to a text node rendered by a React component is with findDOMNode. The docs indicated that the reason findDOMNode hadn't been removed was because there were no alternatives to use cases like this.

This seems like an extremely narrow edge case (why would you need a ref to a text node, right?), but @nytimes/react-prosemirror is heavily reliant on this API. Because this is a rich text editing library, we can't wrap text nodes with ref-able elements without introducing complexity/edge cases.

What should we do here? I understand the desire to remove a long-deprecated API, but React ProseMirror is now somewhat in a lurch.

A simplified version of how React ProseMirror uses findDOMNode:

export class TextNodeView extends Component<Props> {
  private viewDescRef: null | TextViewDesc | CompositionViewDesc = null;
  private renderRef: null | JSX.Element = null;

  updateEffect() {
    const { view, decorations, siblingsRef, parentRef, getPos, node } =
      this.props;
    // There simply is no other way to ref a text node
    // eslint-disable-next-line react/no-find-dom-node
    const dom = findDOMNode(this);

    let textNode = dom;
    while (textNode.firstChild) {
      textNode = textNode.firstChild as Element | Text;
    }

    // We construct a view descriptor tree to integrate with ProseMirror.
    // This is essentially ProseMirror's virtual DOM implementation. It
    // needs to contain references to each node that it's responsible for,
    // just like the React virtual DOM.
    if (!this.viewDescRef) {
      this.viewDescRef = new TextViewDesc(
        undefined,
        [],
        () => getPos.current(),
        node,
        decorations,
        DecorationSet.empty,
        dom,
        textNode
      );
    } else {
      this.viewDescRef.parent = parentRef.current;
      this.viewDescRef.children = [];
      this.viewDescRef.node = node;
      this.viewDescRef.getPos = () => getPos.current();
      this.viewDescRef.outerDeco = decorations;
      this.viewDescRef.innerDeco = DecorationSet.empty;
      this.viewDescRef.dom = dom;
      // @ts-expect-error We have our own ViewDesc implementations
      this.viewDescRef.dom.pmViewDesc = this.viewDescRef;
      this.viewDescRef.nodeDOM = textNode;
    }

    if (!siblingsRef.current.includes(this.viewDescRef)) {
      siblingsRef.current.push(this.viewDescRef);
    }

    siblingsRef.current.sort(sortViewDescs);
  }

  shouldComponentUpdate(nextProps: Props): boolean {
    return !shallowEqual(this.props, nextProps);
  }

  componentDidMount(): void {
    this.updateEffect();
  }

  componentDidUpdate(): void {
    this.updateEffect();
  }

  componentWillUnmount(): void {
    const { siblingsRef } = this.props;
    if (!this.viewDescRef) return;
    if (siblingsRef.current.includes(this.viewDescRef)) {
      const index = siblingsRef.current.indexOf(this.viewDescRef);
      siblingsRef.current.splice(index, 1);
    }
  }

  render() {
    const { node, decorations } = this.props;

    // This may wrap the text in, e.g., a span,
    // but usually returns a string
    return decorations.reduce(
      wrapInDeco,
      node.text
    );
  }
}

Prior discussions:
#28926
#14357

Thanks, @eps1lon. I'll close this in favor of #28926, and add my comment there.