Pier
Pier

Reputation: 10827

What's the proper way of passing a ref to a prop?

I'm trying to pass a ref of a component to another component. Since string refs are being deprecated I'm using callback refs.

So I have something similar to this:

<One ref={c => this.one = c}/>
<Two one={this.one}/>

The problem is that whenever I try to access this.props.one inside Two I get undefined.

I have even tried this on Two:

componentDidMount(){
    setTimeout(()=>{
        console.log(this.props.one);
    },5000)
}

It seems the problem is that when the prop is created, the ref doesn't exist yet since it's created once One is mounted. But I don't know how to "refresh" the props on Two to get the ref to the mounted component.

So what's the proper way of passing a ref to another component?

Edit

Some users have suggested to encapsulate that logic in a higher component, which in itself renders those other child components.

The problem with that approach is that you can't create reusable logic and you have to repeat the same logic over and over in those encapsulating components.

Let's say you want to create a generic <Form> component which encapsulates the submit logic to your store, error checking, etc. And you do something like this:

<Form>
    <Input/>
    <Input/>
    <Input/>
    <Input/>
    <SubmitButton/> 
</Form>

In this example <Form> can't access the instances (and methods) of the children since this.props.children doesn't return those instances. It returns some list of pseudo components.

So how can you check if a certain <Input/> has detected a validation error without passing a ref?

You have to encapsulate those components in another component with the validation logic. For example in <UserForm>. But since each form is different the same logic has to be copied in <CategoryForm>, <GoupForm>, etc. This is terribly inefficient which is why I want to encapsulate the validation logic in <Form> and pass references of the <Input> components to <Form>.

Upvotes: 48

Views: 87147

Answers (4)

kevin.groat
kevin.groat

Reputation: 1294

This is now much simpler using the new ref api (available since React 16 - thanks to perilandmishap for pointing that out).

class MyComponent extends React.Component {
  constructor (props) {
    super(props);
    this.oneRef = React.createRef();
  }

  render () {
    return (
      <React.Fragment>
        <One ref={this.oneRef} />
        <Two ref={this.oneRef} />
      </React.Fragment>
    }
  }
}

You would consume the prop in <Two/> like:

this.props.one.current

A few things of note with this approach:

The ref will be an object with a current property. That property will be null until the element/component is mounted. Once it's mounted, it will be the instance of <One/>. It should be safe to reference it once <Two /> is mounted.

Once the <One /> instance is unmounted, the current property on the ref returns to being null.

Upvotes: 13

<One ref={c => this.one = c}/>
<Two one={this.one}/>
class One extends React.Component {
  render(){
    return <div ref={this.props.one}/>;
  }
}

export default React.forwardRef((props, ref) => (
  <One {...props} {...ref.current} />
));

The ref prop is for elements with forwardRef in its own file.

class IndexApp extends React.Component {
  constructor (props){
    super(props);
    this.one = React.createRef();
  }
  render (){
    return <div>
      <One ref={{current:{one:this.one}}}/>
      <Two ref={{current:{one:this.one}}}/>
    </div>;
  }
}

Upvotes: 0

Ben Mosher
Ben Mosher

Reputation: 13381

In general, if you need to pass a reference to something that may not be set at call time, you can pass a lambda instead:

<One ref={c => this.one = c}/>
<Two one={() => this.one}/>

and then reference it as

this.props.one()

If it has been set when you call it, you'll get a value. Before that, you'll get undefined (assuming it hasn't otherwise been initialized).

It bears noting that you won't necessarily re-render when it becomes available, and I would expect it to be undefined on the first render. This is something that using state to hold your reference does handle, but you won't get more than one re-render.

Given all that, I would recommend moving whatever code was using the ref to One in Two up into the component that is rendering One and Two, to avoid all the issues with both this strategy, and the one in @Carl Sverre's answer.

Upvotes: 7

Carl Sverre
Carl Sverre

Reputation: 1188

In general the "ref" feature is an anti-pattern in React. It exists to enable side-effect driven development, however in order to benefit the most from the React way of programming you should try to avoid "refs" if possible.

As for your particular issue, passing a child a ref to it's sibling is a chicken vs. egg scenario. The ref callback is fired when the child is mounted, not during render which is why your example doesn't work. One thing you can try is pushing the ref into state and then reading from state into the other child. So:

<One ref={c => !this.state.one && this.setState({ one: c })}/>
<Two one={this.state.one}/>

Note: without the !this.state.one this will cause an infinite loop.

Here is a codepen example of this working (look at the console to see the sibling ref logged): http://codepen.io/anon/pen/pbqvRA

Upvotes: 32

Related Questions