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

Get props.children DOM handle: React.findDOMNode(child) #4244

Closed
MadLittleMods opened this issue Jun 29, 2015 · 23 comments
Closed

Get props.children DOM handle: React.findDOMNode(child) #4244

MadLittleMods opened this issue Jun 29, 2015 · 23 comments

Comments

@MadLittleMods
Copy link
Contributor

When trying to use React.findDOMNode on a props.children item(ReactElement), an error is thrown.

Uncaught Error: Invariant Violation: Element appears to be neither ReactComponent nor DOMNode

Here is a small demo that shows off the problem.

React.Children.forEach(this.props.children, (child) => {
    console.log(React.findDOMNode(child));        
});

The parent component is mounted console.log('isMounted', this.isMounted()); -> isMounted true.


Related to #1602 and #1373.

Additionally the behavior of didMount and didUpdate handlers are currently undefined in regards to when they fire in relation to their children and therefore refs. For example componentDidUpdate is not guaranteed to fire after the children has fully mounted.

From sebmarkbage, Source

@sophiebits
Copy link
Collaborator

This behavior is correct. If you want to find one of your children's DOM nodes, you have to add a ref first (using React.cloneElement) and then use that ref. (An element is little more than the type and props of what you want to render; it doesn't have identity and doesn't correspond to a DOM node directly.)

@sophiebits
Copy link
Collaborator

(The next commend in the thread you linked contradicts that. We do guarantee that children have their componentDidMount/DidUpdate called before the parent gets it.)

@MadLittleMods
Copy link
Contributor Author

@spicyj Thank you for the info and correction.

Here is a working demo with React.cloneElement. React.addons.cloneWithProps is deprecated:

// `render`
React.Children.map(this.props.children, (child) => {
    return React.cloneElement(child, {
        ref: `item-${child.props.key}`
    });
});

// ...

// `componentDidMount` or `componentDidUpdate`
React.Children.forEach(this.props.children, (child) => {        
    console.log(React.findDOMNode(this.refs[`item-${child.props.key}`]));        
});

@mikkoh
Copy link

mikkoh commented Nov 30, 2015

@spicyj how would you handle a situation where some child component should find a dom node down stream?

For instance:

render() {
  return <SomethingThatWillFindStuff>
    <div ref="parentDiv">
      <div ref="iShouldBeFound">find me</div>
    </div>
  </SomethingThatWillFindStuff>;
}

SomethingThatWillFindStuff cannot simply clone <div ref="iShouldBeFine">find me</div> as <div ref="parentDiv">'s props are obviously immutable.

@VinSpee
Copy link

VinSpee commented Jan 22, 2016

I'm wondering the same thing as @mikkoh, any solution?

@midsbie
Copy link

midsbie commented Feb 18, 2016

@spicyj Also looking for an answer to the question @mikkoh asked.

@midsbie
Copy link

midsbie commented Feb 18, 2016

@spicyj It may not be possible or feasible to add refs to children nodes, in which case how does one query a DOM node downstream from a .props.children reference?

@jimfb
Copy link
Contributor

jimfb commented Feb 18, 2016

@miguel-guedes @VinSpee @miguel-guedes: Look at the solution @MadLittleMods provided, his solution is correct.

SomethingThatWillFindStuff cannot simply clone <div ref="iShouldBeFine">find me</div> as <div ref="parentDiv">'s props are obviously immutable.

Immutable props does not prevent cloning.

It may not be possible or feasible to add refs to children nodes, in which case how does one query a DOM node downstream from a .props.children reference?

If you use callback refs, it's always feasible, because you can have the new callback ref also call the previous callback ref, making it completely transparent. Or you can have whomever does have the ref pass down a reference to the actual dom node. Or you can call ReactDOM.findDOMNode(this) and walk the DOM. The recommended solution is to attach a ref.

Having said all that... Usage questions are better answered on sites like StackOverflow, as we try to use github issues for tracking bugs in the React core. We generally do not answer usage questions on github.

@zhujun24
Copy link

@MadLittleMods if some child has his own ref attribute, and some others child without this attribute,

return React.cloneElement(child, {
    ref: `item-${child.props.key}`
});

item-${child.props.key} can override child own ref ? or how to get child own ref ?

@PierBover
Copy link

Unfortunately it's not possible to add a ref to a functional component, so how can we find the DOM node of a children that is a functional component?

@iamyoki
Copy link

iamyoki commented Apr 30, 2021

I have two buttons inside Tooltip Component, and I don't want to give a wrapper for btns and also could get the DOMs from children.
Here is the solution:

  1. Use React.Children.map & React.cloneElement to give every child an ref.
  2. const an refs = useRef([]) above
  3. Inside child props, ref: dom=>refs.current[i]=dom
    image

@JuanC-JC
Copy link

Actually i have a error with this method, maybe is not avaliable for the last version of react?
image
image

@iamyoki
Copy link

iamyoki commented Jul 15, 2021

Actually i have a error with this method, maybe is not avaliable for the last version of react?
image
image

It's because you can pass ref to built-in DOM elements but you can't pass ref to your components directly.
Here is how to pass ref to a child component.

@trompx
Copy link

trompx commented Jun 17, 2022

Hey @iamyoki,

Doing what you did works when I have one Wrapper:

<WrapperOne>
   {props.children}
</WrapperOne>

But in my case I need to nest wrap props.children:

<WrapperOne>
  <WrapperTwo>
     {props.children}
  </WrapperTwo>
</WrapperOne>

Where each Wrapper are functional components that add some logic (bind events, set classes etc.). In that case, I can get the ref in WrapperTwo with React.cloneElement but in WrapperOne I can't, the ref is null. Is it possible to use ReactDOM.findDOMNode in WrapperOne? Or is there another best practice to wrap props.children and add logic without wrapping with an extra <div> inside each Wrapper?

@iamyoki
Copy link

iamyoki commented Jun 17, 2022

Hey @iamyoki,

Doing what you did works when I have one Wrapper:

<WrapperOne>
   {props.children}
</WrapperOne>

But in my case I need to nest wrap props.children:

<WrapperOne>
  <WrapperTwo>
     {props.children}
  </WrapperTwo>
</WrapperOne>

Where each Wrapper are functional components that add some logic (bind events, set classes etc.). In that case, I can get the ref in WrapperTwo with React.cloneElement but in WrapperOne I can't, the ref is null. Is it possible to use ReactDOM.findDOMNode in WrapperOne? Or is there another best practice to wrap props.children and add logic without wrapping with an extra <div> inside each Wrapper?

@trompx Of course, if you want to get props.children from inside WrapperOne you can recursively map and clone children until you get them.

@trompx
Copy link

trompx commented Jun 17, 2022

Thanks for the prompt answer @iamyoki.
Do you mind showing what WrapperOne would look like? I couldn't get the ref working.

const ref = useRef();
const clone = React.Children.map(children, (child) => {
  // By looping once more, I manage to get the initial props.children that I would get if I
  // didn't have WrapperTwo. But I get undefined in useEffect.
  React.Children.map(child.props.children, (nestedchild) => {
    return React.cloneElement(
      nestedchild,
      {
        ref: (dom) => (ref.current = dom),
      }
    );
  });
});
useEffect(() => {
  console.log(ref);
}, []);

In the meantime, I found another way.

const oneRef = useWrapperOne();
const twoRef = useWrapperTwo();
const mergedRef = useComposeRefs(oneRef, twoRef);

return (
  <>
    {React.Children.map(children, (child) =>
      React.cloneElement(child, {
        ref: mergedRef
      })
    )}
  </>
);

But I am still wondering how to do it the way I mentioned before.

@iamyoki
Copy link

iamyoki commented Jun 21, 2022

@trompx Sure.
Define a function to traverse every child and overlay it's props with callback's return

function traverseMap(nextChildren, callback) {
  return React.Children.map(nextChildren, child=>({
    ...callback && callback(child),
    children: child.props.children && traverseMap(child.props.children, callback)
  }))
}

Find your target comp by name and what u want

traverseMap(children, child=>{
  if(child.type.name === 'WrapperTwo' || child.displayName === 'WrapperTwo') {
    return {
      ...child,
      props: {
        ...child.props,
        ref: dom => {if(dom) ref.current = dom}
      }
    }
  }
  
  return child
})

Complete code

function traverseMap(nextChildren, callback) {
  return React.Children.map(nextChildren, child=>({
    ...callback && callback(child),
    children: child.props.children && traverseMap(child.props.children, callback)
  }))
}

traverseMap(children, child=>{
  if(child.type.name === 'WrapperTwo' || child.displayName === 'WrapperTwo') {
    return {
      ...child,
      props: {
        ...child.props,
        ref: dom => {if(dom) ref.current = dom}
      }
    }
  }
  
  return child
})

By the way, I didn't verify the code, I just draft it out, so you can correct it if any bugs here.

@gaearon
Copy link
Collaborator

gaearon commented Jun 21, 2022

I strongly recommend against doing invasive transformations like this. This kind of code is brittle and difficult to use and understand. Wrap the children in a <div> and get a ref to that div explicitly instead.

@iamyoki
Copy link

iamyoki commented Jun 21, 2022

I strongly agree with Dan :)

@trompx
Copy link

trompx commented Jun 21, 2022

Thanks for the input @gaearon, but I often have to wrap components with 2-5 of those wrapper components and I'd rather avoid to add the same amount of extra div for each of them. That's unfortunate there is no easy way to get the ref on nested components. Using hooks and merging refs works perfectly in my case, so if this approach is not considered invasive, I'll go with it.
Anyway thanks @iamyoki for taking the time, I really appreciate.

@gaearon
Copy link
Collaborator

gaearon commented Jun 21, 2022

What do these wrappers do?

@trompx
Copy link

trompx commented Jun 23, 2022

The goal was to add interactivity to some elements, make them react to different events/actions: hover/click/load/onscreen/scroll etc.
Like:

<HoverWrapper
  animateIn={rollhoverIn}
  animateOut={rollhoverOut}>
  <Component>
</HoverWrapper>

Some elements need only one wrapper but some more. I wanted to have each in its own either component or hook for readability.
So I have a MainWrapper that would either:

  1. nest wrap those wrappers conditionally if I enable specific action at the component level.
  2. compose a ref using hooks as I stated in my previous answer which works fine, using Reac.cloneElement (to really have no extra div at all), but I could also use a div instead (at least I would have only one, compared to having one in each Wrapper component).

But maybe there is a better approach?

@iamyoki
Copy link

iamyoki commented Jun 24, 2022

@trompx I think you should redesign the use case. Like this.

<Animation>
    {({step, rollhoverInProps, rollhoverOutProps}) => (
      <div {...(step === 'in' ? rollhoverInProps : rollhoverOutProps)}>...</div>
    )}
  </Animation>

Also, you can try a library transition-hook that I wrote.

const [onOff, setOnOff] = useState(true)
const {stage, shouldMount} = useTransition(onOff, 300) // (state, timeout)

return <div>
  <button onClick={()=>setOnOff(!onOff)}>toggle</button>
  {shouldMount && (
    <p style={{
      transition: '.3s',
      opacity: stage === 'enter' ? 1 : 0
    }}>
      Hey guys, I'm fading
    </p>
  )}
</div>

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

No branches or pull requests