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

How Should Refs Work? #3234

Closed
sebmarkbage opened this issue Feb 22, 2015 · 5 comments
Closed

How Should Refs Work? #3234

sebmarkbage opened this issue Feb 22, 2015 · 5 comments

Comments

@sebmarkbage
Copy link
Collaborator

Spinoff discussion of #1373, #3128 and #3228.

We're pretty sure that we need to change the current way refs work (see above). However, the new ref callbacks are also not ideal because it relies on some imperative code and suffers from timing issues like all imperative life-cycles. It is also not very convenient without arrow functions. Even with arrow functions it might suffer from performance issues.

A few things refs tries to solve:

  • Imperative access to APIs that doesn't have declarative counter-parts yet. E.g. DOM layout.
  • Fast-path down the tree to update deeply to by-pass reconciliation. Perf-optimization.
  • Get the current state of a child that you don't want to control without duplicating state (which might possibly diverge).
  • Send signals/pulses downwards.
@sebmarkbage
Copy link
Collaborator Author

Another one that I've been thinking about is "parent-based refs".

You could treat refs similar to context and assign it a unique key that bubbles upwards through the parent hierarchy and everyone in the hierarchy gets access to it. Effectively reversed context. Which is useful for other things too: https://github.com/reactjs/react-future/tree/master/04%20-%20Layout

var DIV = new Symbol();
class Container {
  componentDidMount() {
    var myDiv = this.refs[DIV];
    ...
  }
  render() {
     return <Something><div ref={DIV}>{this.props.children}</div></Something>;
  }
}

This should work for most cases but it has the possibility of screwing you up when they're nested:

class Something {
  render() {
     if (stopRecursion) return this.props.children;
     return <Container>{this.props.children}</Container>;
  }
}

It would provide a way to identify upwards signals such as this proposal:

https://github.com/reactjs/react-future/blob/master/09%20-%20Reduce%20State/01%20-%20Declarative%20Component%20Module.js#L19

Anyway... Just an idea to spin-off other ideas.

@syranide
Copy link
Contributor

As I kind-of-mentioned in #3228, I suspect part of the answer is:

  1. Refs are well-suited for event-triggered interaction with uncontrolled components (clicking save and saving, etc).
  2. Whereas refs aren't well-suited for life-cycle events; refs are based on your own life-cycle whereas you're really interested in the life-cycle of the target, you don't want a ref, you just want the instance momentarily and only when the target mounted/updated/unmounts.

PS. That's obviously not the complete story, but it seems to me that refs for life-cycle events might not be the right way to go.

@gaearon
Copy link
Collaborator

gaearon commented Apr 7, 2015

Another one that I've been thinking about is "parent-based refs".

I'm actually using a similar pattern in the next version of React DnD for attaching nodes to the higher-order component that binds drag and drop events.

class Box extends Component {
  static propTypes = {
    dragPreviewRef: PropTypes.func.isRequired,
    dragSourceRef: PropTypes.func.isRequired
  };

  render() {
    const { dragSourceRef, dragPreviewRef } = this.props;

    // Specify draggable node and drag preview node
    // by using refs provided by higher-order component.

    return (
      <div ref={dragPreviewRef} className='draggable'>
        <div ref={dragSourceRef} className='dragHandle' />
        Drag me by the handle
      </div>
    );
  }
}

export default configureDragDrop(Box, {
  configure: (register) =>
    register.dragSource(ItemTypes.BOX, boxSource),

  collect: (connect, monitor, dragSourceId) => ({

    // Refs are provided by higher-order component:

    dragPreviewRef: connect.dragSourcePreview(dragSourceId),
    dragSourceRef: connect.dragSource(dragSourceId)
  })
});

I've come to really like this API because it provides flexibility to compose refs:

function joinRefs(refA, refB) {
  return function (instance) {
    refA(instance);
    refB(instance);
  };
}

class Card {
  render() {
    const { caption, dragSourceRef, dropTargetRef } = this.props;

    return (
      <div ref={joinRefs(dragSourceRef, dropTargetRef)}>
        {caption}
      </div>
    );
  }
}

Also I can always do it more manually for more tricky use cases (such as specifying a custom Image as drag preview):

class Box extends Component {
  componentDidMount() {
    const dragPreview = new Image();
    dragPreview.onLoad = () => this.props.dragPreviewRef(dragPreview);
    dragPreview.src = 'http://image.jpg';
  }

  render() {
    const { dragSourceRef } = this.props;

    return (
      <div className='draggable'>
        <div ref={dragSourceRef} className='dragHandle' />
        Drag me by the handle
      </div>
    );
  }
}

@fdecampredon
Copy link

I have some limitation with the current ref function.
I often use a pattern where I am using ref for some sort of external controller that helps me implementing some behaviors on my component.
For example I have a DropDownController which takes a change callback as parameter and exposes 2 refs functions and will control when a dropdown should be open or closed:

class MyComponent extends React.Component {
  state = { open: false }
  ddc = DropDownController(open => this._onChange(open));

  _onChange(open)  {
    this.setState({ open });
  }

  render() {
   return (
    <div>
      <button ref={this.ddc.openButtonRef}>+</button>
      {this.state.open &&
        <div ref={this.ddc.dropDownRef}>....</div>}
    </div>
  }
}

The controller manually creates event handlers and the dropdown is closed if a click event happen outside of the dropdown.
That's working fine, but sometimes I would like to use those same refs for multiple elements that should also trigger the dropdown opening, or not triggering the dropdown closing.
Unfortunately if I do so I won't be able to clean my events handler correctly since ReactRef doesn't pass the instance of the component being unmounted when ref are detached.

One solution could be that the ref function is invoqued with null as first parameter but also the instance of the unmounted component as second parameter.
I monkey patched ReactRef to do so and it's working fine, but I would prefer to be able to avoid monkey patch.

@sebmarkbage
Copy link
Collaborator Author

hm. I'm currently working on view recycling which allow us more efficient use of resources in long lists or tabs that share similar behaviors.

One of the interesting qualities of that problem is that instances are no longer necessarily tied to the state of a component. I'm not sure what that means for this particular problem yet but I suspect that relying on instance identity here might be problematic. Maybe not though.

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

No branches or pull requests

5 participants