Open eps1lon opened 5 years ago
styled-components used findDOMNode as a debug assist to verify if your wrapper component does eventually forward the className to a DOM node.
However, as is the case with all of these uses of findDOMNode, it'll only work correctly if only one DOM Element is eventually rendered, or if the appropriate element is the first one rendered. Any fragment siblings are completely invisible to it. This is why it's deprecated.
As I commented in https://github.com/styled-components/styled-components/issues/2154#issuecomment-444031377, let's track this issue to enumerate use cases that are addressed by findDOMNode
but not by refs. Please comment with a small demo and a description of what and why you're trying to do. Note that it takes time and effort to understand your particular problem so please try to explain what kind of API you're trying to implement. Don't assume we're familiar with your library. Thanks!
I have a project - visual editor for composing layouts with React components ui library by dragging components with mouse.
This React components are developed by different team so we don't have a direct access to modify their codebase and for now passing refs to that components is not supported. And we can use different React UI libraries by different developer because with findDOMNode
we can work with them universally independent of supporting props with forwarding refs .
In our project codebase, we wrap each of that components with several HOCs. Each HOC contains some code with findDOMNode - for adding event listeners like click/mousemove or for using getBoundingClientRect()
method.
So findDOMNode
is the only option for us.
@gaearon
About styled-components
:
findDOMNode
used only in dev environment for warn users of incorrect using of styled-components. It's very helpful feature, because a lot of beginners stumble over this issue.
Details of usage:
That's issue can be reproduced when custom React component is passed to styled() factory. In this case styled-components create className
and pass it as prop to that custom React component. If component is not supposed of supporting className
prop, warn of incorrect usage should be thrown.
So, in styled-components code, we need to find DOM node with generated className.
If it exist then everything is OK. But we can't get our users to use ref forwarding on each their component. We need universal approach to finding child DOM node, that's why findDOMNode is used.
A use case that i think of, is when you want to perform some logic on the underline DOM
element of the child.
For example, a component that wraps any other component and traps events to check if its "outside" of this child. Like click
or mouseover
etc.
This is the relevant logic that depends on the ref:
handleEvent = ({ target }) => {
const trapped = this.ref.contains(target);
this.setState({ trapped });
};
With findDOMNode
its easy and flexible to get a reference to the child no matter what it is. let it be a function, a class or a text.
componentDidMount() {
const { event } = this.props;
this.ref = ReactDOM.findDOMNode(this);
document.addEventListener(event, this.handleEvent);
}
The alternative (hooks aside) is to expose the ref callback to the user (via render prop for example):
render() {
const { children } = this.props;
const { trapped } = this.state;
return children(trapped, ref => (this.ref = ref));
}
And the user will attach it to what ever DOM
element he/she wants
{(trapped, refCallback) => (
<div ref={refCallback} >
<div>{trapped ? "open" : "close"}</div>
</div>
)}
This will work but the API is kind of awkward, plus we shift the responsibility for dealing with refs to the end user (sort of implementation details).
A more critical problem with this API is that it may conflict with other components that are using this approach.
Consider a second library like ElementResize
that needs a ref to calculate some resize logic on it, and this library is also exposing a ref callback.
<ElementResize>
{(rect, refCallback) => (
<div ref={refCallback}>
<div>{rect.width}</div>
</div>
)}
</ElementResize>
If a user wants to use both libraries, he/she can't use both callbacks on the same element (again, hooks aside):
<ElementResize>
{(rect, refCallback1) => (
<Trap event="click">
{(trapped, refCallback2) => (
<div ref={???}> <--- can't use 2 ref callbacks on the same element
<div> {trapped ? 'focused' : 'blur'}</div>
<div>{rect.width}</div>
</div>
)}
</Trap>
)}
</ElementResize>
I made a running example of the 2 approaches
A ref on a fragment can solve these issues as the library can render a "transparent" element yet keep a ref without the end user has to know or worry about it.
This use case is similar to the use case of react-transition-group
. I don't remember the architecture, but either TransitionGroup
or CSSTransition
use findDOMNode
to get a reference to the underlying DOM node, so they can apply CSS classes based on the transition state and even delay the React unmount when the exit state is present.
The delaying is done, I believe, with cloneElement; but the className
manipulation still requires findDOMNode
.
@Fer0x Thanks for explaining the styled-component
use case. I replied with some alternative ideas in https://github.com/styled-components/styled-components/issues/2154#issuecomment-445049337, happy to continue the discussion about that particular use case there.
I'd say this api is quite nice. I actually would pass ref to tooltip via props which is the easiest way to reuse this ref in another component or hook.
in some case, if needs to get postion of a component which is from a third lib.
first I need to use findDOMNode
to get the DOM element from this component,
so I can call getBoundingClientRect
method somewhere later.
I think that forwardRef
can't doing this.
@liuyangc3 https://github.com/reactjs/rfcs/pull/97
@Kovensky thanks, I will look into this
I have a use case where I've wrapped the excellent @bvaughn 's react-window components with my own in order to force repainting on mousewheel scroll. To do this I do the following:
render() {
const {props: {syncScroll, ...props}} = this
return <FixedSizeList {...props} ref={this.setListRef} />
}
scrollNode: HTMLDivElement
unregisterScrollEvent = () => {
this.scrollNode && this.scrollNode.removeEventListener("wheel", this.onWheel)
this.scrollNode = null
}
list: FixedSizeList
setListRef = (list: FixedSizeList) => {
this.list = list
if (this.props.syncScroll) {
if (!list) {
this.unregisterScrollEvent()
}
else {
this.scrollNode = ReactDOM.findDOMNode(list) as HTMLDivElement
this.scrollNode.addEventListener("wheel", this.onWheel)
}
}
}
How would you suggest I approach this instead?
I have a use case that the tooltip didn't properly be tethered to menu item inside dropdown that is tethered to a dropdown button. We used Tether and plan to move to Popper but it is not clear Tether is the cause. We found that we can use MutationObserver DOM API to detect DOM mutation and force Tether's global position handler to run and solve this problem. However, we want the MutationObserver to observe a smaller subtree of DOM below a "node", instead of the whole DOM. To do this, we will need to call observe(node) and we need to use findDOMNode to get the node.
I have a problem that I have so far not been able to solve using refs, but can solve very nicely with findDOMNode
.
I have a fairly complex custom library, but for brevity it can be reduced to the following:
import React from "react";
export default function App() {
return (
<Module styles={{ background: 'lightgrey' }}>
<Module styles={{ color: 'red' }}>Some div</Module>
<Module styles={{ color: 'blue' }}>Another div</Module>
</Module>
);
}
class Module extends React.Component {
constructor(props) {
super(props);
this.REF = React.createRef();
this.TAG = props.as || 'div';
}
componentDidMount() {
this.paint(this.REF.current, this.props.styles);
}
// in reality this.paint() does much more than re-construct the
// input styles object as shown here
paint(node, styles = {}) {
for (const [prop, val] of Object.entries(styles)) {
node.style[prop] = val;
}
}
render() {
return (
<this.TAG ref={this.REF}>{this.props.children}</this.TAG>
);
}
}
Essentially, within componentDidMount
of my custom <Module>
Component, I need the underlying DOM node so I can pass it to this.paint()
(which requires the underlying DOM node).
So far, this works fine using refs. The issue occurs when attempting to pass other React Components to the as
prop, so that this.TAG
is some other React Component instead of a div
.
In my case, I am trying to pass the Components from Pure React Carousel to my custom <Module>
Component. The relevant part from the above code would now be:
...
import { CarouselProvider, Slider, Slide } from 'pure-react-carousel';
import 'pure-react-carousel/dist/react-carousel.es.css';
export default function App() {
return (
<CarouselProvider naturalSlideWidth={4} naturalSlideHeight={4} totalSlides={2}>
<Module as={Slider} styles={{ background: 'lightgrey' }}>
<Module as={Slide} styles={{ color: 'red' }}>Slide 1</Module>
<Module as={Slide} styles={{ color: 'blue' }}>Slide 2</Module>
</Module>
</CarouselProvider>
);
}
...
The code inside the componentDidMount()
method of <Module>
now breaks because this.REF.current
now points to the host Component instead of the DOM node. This can be fixed by changing the componentDidMount()
method to:
componentDidMount() {
this.paint(findDOMNode(this.REF.current), this.props.styles);
}
I would love to know if what I am attempting can be achieved with only refs. Thanks.
There is an application scenario, there is a component Overlay, which needs to be attached to a third-party component and obtain the position of the DOM element of the third component, such as: https://github.com/react-bootstrap/react-bootstrap/blob/master/src/OverlayTrigger.js#L122
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!
Big fat bump, stale bot sucks
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!
We are using an internal-developed WYSIWYG as a developer framework and cms. It's entire logic is written around findDomNode. Basically all the reusable (random-sourced) components, are wrapped around with an abstract 'wrapper', that attaches all kids of handlers from mouseEnter to drop to the domNode created by that component. As a result, when hovering any component in the tree, you can bubble up to closest library ui-component, then you build a node tree and can modify it separately under any resolution, ab-testing condition or any other context.
This case has been stated quite a few times and it's entirely impossible to do via refs. IMO wysiwyg cms as the development tool is actually the next step in applying redux to build web-applications, as the CMS itself may take away all the extra logic, adding editor-layers, such as: Adaptivity, Language, ContentMapping, Extra Styling, AB-Testing, WebAnalytics, WebOptimization etc, and even move e2e testing to a whole new level, by allowing to record testing cases with attachment to concrete nodes.
So we're sincerely hoping findDOMNode will always be a viable option. Moving it into a separate package but still supporting in react, just like prop-types seems like the best options here.
Fragment event handlers (#12051) would be good to have. It would allow replacing findDOMNode()
usages that are only done to add event listeners without needing to add an extra DOM element.
React 19 is about to be released. I was wondering if there's any update on this issue?
I've noticed that React internally still retains findDOMNode, they just don't export it. You can still access it via ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.findDOMNode
I have released a polyfill package at https://www.npmjs.com/package/find-dom-node-polyfill. If you want to test upgrading to React 19, you can try using this package. It essentially replicates the functionality of findDOMNode from the React source code.
@childrentime I've hidden your comment - publishing packages that use React internals like this will block unsuspecting users from upgrading, please do not do this.
OK. Really sorry about that.
import React, { useRef, useEffect } from 'react';
interface IProps {
children?: React.ReactNode;
as?: keyof JSX.IntrinsicElements | React.ComponentType<any>;
}
const ExposureWrapper: React.FC<IProps> = ({ children, as: Component = 'div', ...rest }) => {
const ioRef = useRef<IntersectionObserver | undefined>();
const domRef = useRef<any>(null);
useEffect(() => {
const domElement = domRef.current;
console.log('domElement', domElement)
if (!ioRef.current) {
ioRef.current = new IntersectionObserver(handleVisibilityChange);
}
const io = ioRef.current;
if (domElement && io) {
io.observe(domElement);
}
return () => {
if (domElement && io) {
io.unobserve(domElement);
io.disconnect();
}
};
}, []);
if(typeof Component === 'string') {
return React.createElement(Component, { ...rest, ref: domRef }, children);
}else if(typeof Component === 'function') {
// need to assign ref to component's first child
const Result = React.createElement(Component,{ ...rest },children);
return Result;
}
};
export default ExposureWrapper;
function handleVisibilityChange(entries: IntersectionObserverEntry[]) {
console.log('handleVisibilityChange', entries)
}
For me, findDOMNode can easily accomplish this component, but if not used, it seems like there's no other way.
Timeline
findDOMNode
is discouraged but accepted for certain use casesforwardRef
is introduced: It can be used in HOCs to avoid usingfindDOMNode
on the enhanced componentfindDOMNode
is deprecated inReact.StrictMode
React.Concurrent
mode is released: This mode extendsReact.StrictMode
in a way thatfindDOMNode
is deprecated in that mode too.React.Concurrent
modefindDOMNode use cases
If you have more use cases please let me know. I only started with some examples from
mui-org/material-ui
.with a planned alternative
State of
forwardRef
react
has 3.4M downloads/week.hoist-non-react-statics
(3.9M downloads/week; not clear what percentage is 2.x)A utility mainly used in HOCs and encouraged to use in the official react docs. However everyone stuck at
2.x
will likely encounter issues withforwardRef
since that version does not handle anyreact@^16.3
features. ^3.2.0 should have no issues apart from some minor issues with propTypes hoisting fromforwardRef
toforwardRef
. The latest stable from zeit/next still uses that outdated version. However the latest canary for 7.0.3 does not.react-docgen (400k downloads/week)
Not recognized as a valid component definition. PR open at reactjs/react-docgen#311.
react-redux (1.4M downloads/week)
connect
does properly forward their refs in the beta release of 6.x. No timeline for stable release given however 3 betas have already been released so it's probably soon.react-router (1.4M downloads/week)
withRouter
is planned to forward refs (ReactTraining/react-router#6056#issuecomment-435524678). However no comment about the other components and no major release candidate is published.display name
React.forwardRef
components are recognized byreact-devtools
. However when wrapped in a HOC it's very likely that the display name is lost. See facebook/react#14319The issue
Assumptions:
React.ConcurrentMode
If none of those applies to you then you probably don't have an issue with
findDOMNode
deprecation.The mode of a partial tree can only be made more restrictive but not loosened up. If you wrap your tree in
React.StrictMode
and use a component from a 3rd party library that 3rd party library has to beReact.StrictMode
compliant too.This means that you can't use
React.StrictMode
effectiveley. This might be ok since it's for development only anyway and has no implications for production. However Concurrent mode can have actual implications for production. Since it is new and the community wants to use new things libraries have to make sure that they are strict mode compliant too.In addition between the relase of an alternative in the form of
React.forwardRef
and the deprecation only 7 months have passed. One could argue that this is plenty of time but (at least from my perspective) the work on migrating fromfindDOMNode
to refs andforwardRef
was postponed becausefindDOMNode
was not deprecated yet. However the actual deprecation happened one day before the release ofunstable_ConcurrentMode
virtually giving no time to migrate. ~We'll have to see when a stable16.7
release will happen but assuming this happens today only a month has passed between deprecation and virtual removal.~ React 16.x Roadmap was release pointing towards Q2 2019 as a release date of stableReact.Concurrent
mode. This relaxes pressure for library maintainers quite a bit IMO.Conclusion
Refs are not a viable upgrade path to replace
findDOMNode
yet. Until refs are usable without headaches from forwarding refsfindDOMNode
should be undeprecated.Releated