Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[React 19] Removal of ReactDOM.findDOMNode #28926

Open
migueloller opened this issue Apr 26, 2024 · 18 comments
Open

[React 19] Removal of ReactDOM.findDOMNode #28926

migueloller opened this issue Apr 26, 2024 · 18 comments
Labels

Comments

@migueloller
Copy link

Summary

React 19 removes the deprecated ReactDOM.findDOMNode utility. While this utility was an escape hatch and refs are the recommended way to access the DOM, in certain cases, findDOMNode was the only solution using a React API. Let me explain.

I maintain @makeswift/runtime, hosted at https://github.com/makeswift/makeswift, which is the SDK for Makeswift, a visual editor for Next.js and React. Users of Makeswift can register React components with Makeswift so that they're available to drop in the Makeswift builder. We leverage findDOMNode so that we can find registered component's DOM nodes when users don't use forwardRef (or in React 19, handle the ref prop).

Here's how we use findDOMNode: https://github.com/makeswift/makeswift/blob/58f425cf522c23af4a71b2f07a7625b252c59a5e/packages/runtime/src/runtimes/react/find-dom-node.tsx

While we could force users to use forwardRef, a very important product philosophy of Makeswift is that we meet developers where they're at. We don't want them to have to make any changes to their components. Their components shouldn't know about Makeswift, and if they weren't forwarding a ref then introducing Makeswift shouldn't make them do so.

In the same vein, we also don't want to alter the DOM in any way, so alternatives like using a div with display: contents are a no-go for us.

Ideally, we'd be able to use refs with a React.Fragment, but that API doesn't exist yet. This leaves us with the alternative of requiring users to forward refs in their components or reaching for React internals, like React DevTools does to associate a DOM node with rendered components. We'd like to avoid using internals but without findDOMNode, that might be what we have to do since we'd rather do that than force users to have to forward refs.

Would love to hear thoughts and guidance on what the expectation is for library maintainers to do for this use case that can't be handled with refs. Is the suggested approach to use React internals like React DevTools does? Or perhaps support for refs in React.Fragment is on the way?

Thanks!

@eps1lon
Copy link
Collaborator

eps1lon commented Apr 26, 2024

Prior discussion: #14357

@migueloller
Copy link
Author

migueloller commented Apr 26, 2024

Prior discussion: #14357

Thanks for pointing out that discussion @eps1lon. It seems that the visual building case is a common use case. That being said, that issue seems to be quite old and there's not a ton of activity from the React team on that. Do you have any insight on ways to move forward?

I like the idea proposed there about extracting findDOMNode to a separate package, like was done with prop types, so that it's not in react-dom but library maintainers can still leverage it if refs don't cut it.

@childrentime
Copy link

https://react.dev/reference/react-dom/findDOMNode#adding-a-wrapper-div-element

What's troubling me is this: I have an 'Impr' component, which is used as follows:

<Impr><div/></Impr/>

I need to find the root of the child element directly in 'Impr' using findDomNode. I don't want to use a div wrapper because it might disrupt the style layout.

@hlerenow
Copy link

https://react.dev/reference/react-dom/findDOMNode#adding-a-wrapper-div-element

wrapper will impact inner child component style, this is question !!!!

Copy link

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

@github-actions github-actions bot added the Resolution: Stale Automatically closed due to inactivity label Nov 17, 2024
@childrentime
Copy link

bump

@github-actions github-actions bot removed the Resolution: Stale Automatically closed due to inactivity label Nov 17, 2024
@brandonmcconnell
Copy link

brandonmcconnell commented Nov 20, 2024

https://react.dev/reference/react-dom/findDOMNode#adding-a-wrapper-div-element

What's troubling me is this: I have an 'Impr' component, which is used as follows:

<Impr><div/></Impr/>

I need to find the root of the child element directly in 'Impr' using findDomNode. I don't want to use a div wrapper because it might disrupt the style layout.

@childrentime I agree; using a div wrapper wouldn't be ideal here, but you could do so without worrying about any style changes if you use display: contents.

<div style={{ display: 'contents' }}></div>

More info: https://developer.mozilla.org/en-US/docs/Web/CSS/display#contents

@childrentime
Copy link

image Yes, But.....

@smoores-dev
Copy link

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
    );
  }
}

@childrentime
Copy link

I've always been curious about this issue. Publishing this function as a separate package requires little effort - even people unfamiliar with React's source code can do it, since it has few dependencies and only involves some operations and calculations on React's internal structure

@robertpanvip
Copy link

in React 19, findDOMNode has been deprecated, but in some scenarios, we still need similar functionality, such as retrieving a component’s DOM reference or marking specific nodes. A potential solution could be using comment nodes () as placeholders to enable subsequent DOM operations involving these nodes.

Feature Request

We hope React could support rendering comment nodes directly in JSX, such as:

jsx

<div>  
  <!-- This is a comment node -->  
</div>  

Rendering as:

html

<div>  
  <!-- This is a comment node -->  
</div>  

Why This Is Needed

Enhance DOM Manipulation Capabilities
Comment nodes can serve as lightweight markers for advanced DOM operations, such as dynamically inserting content or replacing deprecated findDOMNode functionality.

Flexibility and Compatibility
In some legacy systems, comment nodes are used to identify specific logic. Supporting this feature would allow developers to migrate old projects more easily or implement more complex functionality.

Improve Developer Experience
Allowing HTML comments in JSX can make the code more readable while helping developers document and debug specific logic directly within their JSX code.

Possible Alternatives

If supporting directly is not feasible, React could introduce a component like :
jsx

<div>  
  <Comment>This is a comment node</Comment>  
</div> 

Rendering as:

html

<div>  
  <!-- This is a comment node -->  
</div>  

Conclusion

We kindly request the team to consider this feature.

@agurtovoy
Copy link

For anyone looking for a short- to middle-term workaround, you can resurface findDOMNode from ReactDOM internals, e.g. something along these lines:

import { type Component } from 'react'
import ReactDOM from 'react-dom'

// https://github.com/facebook/react/blob/main/packages/shared/ReactDOMSharedInternals.js
const reactDOMInternals = (ReactDOM as any)
  .__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE as {
  findDOMNode: typeof ReactDOM.findDOMNode
}

export function findDOMNode(instance: Component | null | undefined): Element | Text | null {
  return ReactDOM.findDOMNode
    ? ReactDOM.findDOMNode(instance)
    : reactDOMInternals.findDOMNode(instance)
}

As the __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE name implies, you'll be tied to the specific React version and will have to track ReactDOM changes in this area going forward, but on the plus side you get the exact implementation React still uses internally.

Alternatively, you can assemble your own implementation by using react-reconciler to do most of the heavy lifting for you, but this still leaves you dependent on some of the ReactDOM internals, just in a different way: https://gist.github.com/agurtovoy/0201718e7cda82b705968207190e6929

@childrentime
Copy link

@agurtovoy
image
The same as what I thought before

@agurtovoy
Copy link

@childrentime Yeah, I wouldn't recommend publishing either of these workarounds as a package, but they are okay to apply to your own code as long as you understand the trade offs.

@bitfactory-douwe-devries

I am working on updating a very outdated project and ran in to the same issue. I could not use div wrapper as the element in findDOMNode was used by an external animation package to read properties from said element, and I did not want to restructure the hundred of instances this was used.

class OldWay extends React.Component {
    componentDidMount() {
        this.el = ReactDOM.findDOMNode(this);
        functionThatUsesElementRef(this.el)
    }

    render() {
        return this.props.children;
    }
}

I ended up using React.cloneELement (I tried passing data with a render prop, but for our use case that wasn't feasible)

class NewWay extends React.Component {
    componentDidMount() {
        functionThatUsesElementRef(this.el)
    }

    render() {
        return React.cloneElement(this.props.children, { ref: this.setRef });
    }

    setRef = (element) => {
        this.el = element;
    }
}

Probably not the best solution but it works for now.

@smoores-dev
Copy link

@bitfactory-douwe-devries you'll probably want to check whether the element that you're cloning already has a ref, and set or call it, in addition to yours. Otherwise, you'll break any refs that your consumers try to set themselves.

class NewWay extends React.Component {
    componentDidMount() {
        functionThatUsesElementRef(this.el)
    }

    render() {
        return React.cloneElement(this.props.children, { ref: this.setRef });
    }

    setRef = (element) => {
        const existingRef = this.props.children.props.ref;
        if (typeof existingRef === 'function') existingRef(element);
        if (existingRef) existingRef.current = element;
        this.el = element;
    }
}

Unfortunately, this doesn't work for all of the cases that have been noted here — notably it's still impossible to get a ref to a text node in React 19 :(

@reshetnev-gb
Copy link

In our project we solved the problem of missing findDomNode in this way.

class ExampleComponent extends React.Component {
    _anchorRef = React.createRef();
   _findDomNodeRef = React.createRef(); // the previous value from findDOMNode is stored here 

    constructor(props) {
        super(props);
        this.state = {
              renderAnchor: true
        }
    }
    
    componentDidMount() {
        if (this._anchorRef.current) {
            this._findDomNodeRef.current = this._anchorRef.current.nextElementSibling;
            this.setState({renderAnchor: false});
        }
        ....
    }

    render() {
        return (
             <>
                  {this.state.renderAnchor ?
                      <span ref={this._anchorRef} style={{display: 'none'}} /> : 
                      null
                  }
                  {this.props.children}
             </>
        );
    }
}

@childrentime
Copy link

@reshetnev-gb very imaginary solution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

10 participants