-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Implement Better Refs API #1373
Comments
On top of this we could build a dynamic RefMap that lazily creates ref promises. This allows easy creation of refs for sets of data. It also makes a nicer upgrade path for existing code that assumes that these are just strings: class Foo {
refs = new React.RefMap();
handleTick() {
this.refs.get('myDiv').then(myDivNode => {
this.setState({ width: myDivNode.offsetWidth });
});
}
render() {
return (
<C tick={this.handleTick}>
<div ref={this.refs.get('myDiv')} />
<CustomComponent context={this.refs.get('myDiv')} />
</C>
);
}
} We also probably need to provide a synchronous API as an upgrade path: handleTick() {
var myDivNode = this.myDivRef.doExpensiveWorkRightNowAndLetMeHaveMyNodeNow();
this.setState({ width: myDivNode.offsetWidth });
} Similarly we need this for ref maps. |
This makes a lot of sense to me, though I guess I'm not totally convinced of the need for it to be async. I'd expect most uses to be in mount-ready handlers though I suppose it's possible for a DOM event handler to be called when an update is pending. |
Do you have a plan for what to do with descriptors that are mounted in more than one place? |
Descriptors that are mounted in more than one place should not have refs I guess. Warning maybe? Not only DOM events but we want to batch timers and data fetching events too. The goal being that flush only happens on rAF. Therefore any callback could have the refs pending. 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. |
Actually in the current state, since they're queued on the DOM generation queue, I guess they're guaranteed right now. We may be able to preserve this behavior. Not sure. An alternative API would be to force refs to be extracted in just those two lifecycle hooks. Then you can store them where ever. That might lead to memory leaks. |
On a second thought, didMount/didUpdate is not enough to keep ref handles up-to-date. Since a child can choose to unmount/remount that descriptor at any point. Then those events are not enough. Potentially the ref life-cycle callback could be connected to the owner instance but that's not as flexible as a first class ref I guess. However, if you want something like a resize handle when a child is actually mounted/remounted it might be troublesome to set up a component-level subscription on the child. I.e. you have to call .then at some point before that. However, maybe that should be handled with a more explicit callback? |
Does this mean that if a child never mounts its argument, the ref will "hang" and never resolve? That sounds odd to me. |
Yea... |
I suppose the promise should be rejected if the next flush doesn't lead the ref to be resolved. handleTick() {
this.refs.get('myDiv').then(myDivNode => {
this.setState({ width: myDivNode.offsetWidth });
}, error => {
this.setState({ width: 0 });
});
} |
Presumably the same thing should happen if a ref isn't used at all? I guess that would make a reasonable API, but unless I'm missing something, each ref object won't know which component it belongs to (alternatively, a component won't have a list of all of its refs) and thus can't know when to mark itself as rejected. |
Every ref object can go from resolved to unresolved and back. We track that in the same way as attach/detach ref. E.g. The act of calling for (let ref of pendingRefs) {
if (ref.hasAttachedInstance) ref.resolve(); else ref.reject();
} |
With this API you wrote out, there's no way to get a component instance, only a DOM node; this means you can't call methods, etc. on child components. Intentional? |
Actually I had imagine that a component instance of a ReactDOMComponent could become the DOM node. This is still controversial though. The alternative is just an empty object with a getDOMNode method on it. |
Well sometimes you want a ref to a composite, right? I haven't thought about it much but that sounds like an odd plan to me. The uniform |
Yes, a ref to a composite would be a still be the composite instance, unless it's a stateless component which would not be allowed to have a ref or resolve to null.
It is expected to return a single node but what if your composite is returning a fragment of multiple nodes? It also needs to go on the base class of every composite, even ART composites, render tree composites, MarkDown composites or whatever. Unless every component that wants it is required to opt-in to a special DOM base class. That doesn't guarantee that your ref has the method though. I think a much better API would be |
In any case, we need to figure out what to do with these wrapper components -- if DOM node instances diverge from composites, it's going to be odd when you write |
Yea. I wonder if it can be a pass-through? So that ReactDOMInput does |
An alternative idea... Make descriptors into ref-promises. render() {
var foo = <Foo />;
return <div onClick={() => foo.then(inst => inst.doX())}>{foo}</div>;
} This also provides a nice reset functionality if the ref is ever swapped out. handleClick() {
this.foo.then(inst => inst.doX());
}
render() {
this.foo = <Foo />; // ugh side-effect in render (puke)
return <div onClick={this.handleClick}>{this.foo}</div>;
} This also works nice with multiple owners. |
@sebmarkbage When updating, do refs get called on every rerender? (Or maybe even if shouldComponentUpdate returns false?) |
no, just if it changes. |
So if I render |
No that would be a change in the ref. Just like if you change the ref name. So it should be called. |
In conventional usage though it'll be a different function each time, yeah? |
Good point. Didn't think about that. Maybe just fire if it goes from null to function? |
Closes facebook#1373. Test Plan: jest
看不懂,求翻译 |
good |
The ref API is broken is several aspects.
<Child renderer={index => <div ref="test">{index}</div>} />
-- this ref will be attached where the callback is issued, not in the current owner.I think that the solution must ultimately be some kind of first class ref that can be passed around. These refs can be chained to create multi-owner refs very efficiently. By creating this object for the ref, we can also get rid of keeping track of owners on descriptors. Saving perf for the common idiomatic case of not using refs.
A secondary goal, which may or may not be as important is the idea of making the resolution of refs asynchronous so that you can respond after a batched flush/reconciliation.
The concept of a first class ref is basically a reference to an object that doesn't exist yet. Luckily there's a first class notion of this in the language already... It's called a Promise.
You create a new Ref instance which is just Promise object that will eventually resolve to the actual instance.
Since this builds on top of Promises we would be able to get async/await language features that allow us to do something like this:
This solves all those use cases AFAIK. The asynchronous API is a little difficult to deal with. But it makes it less weird than the alternative when batching is involved.
An unsolved problem is that refs can update to point to a different instance. In that case the Promise would need to be re-resolved. This is why Promises are not good enough and we ultimately need something like an Observable that can handle multiple values. We can't wait for that spec though. Maybe we just allow our promises to be reset and if you call then(...) again, you get a new value?
The text was updated successfully, but these errors were encountered: