I don't see the way to obtain the top level element with the new async ReactDOM.render
Previously ReactDOM.render
would return the top level component:
const component = ReactDOM.render(<Foo />, container);
Now there is the third argument with callback which does not pass the rendered component into callback.
ReactDOM.render(<Foo />, container, (component) => {
assert(component, "boo!"); // ALWAYS boo!
});
I think it is a bug that top level component is not returned in callback.
Previously ReactDOM.render would return the top level component:
Is that no longer the case? My impression is that the return value still works.
That said, the component was never passed to the callback you鈥檙e referring to as an argument. (It is passed as this
but that鈥檚 mostly historical curiosity.)
I would write this code either as:
const component = ReactDOM.render(<Foo />, container);
// Do something with component
which should still work, or as
let component;
ReactDOM.render(<Foo ref={c => component = c} />, container, () => {
// Do something with component
});
which is a bit more future proof and flexible (e.g. it lets you access nested components too).
Hope this helps!
@gaearon Thank you for your answers!
ReactDOM.render
still returns the component but we have eslint rules in place that throw warnings when it's used in such fashion (since it's not future proof). So I have to suppress those manually which is not optimal.return ReactDOM.render(instance, container); // eslint-disable-line react/no-render-return-value
ref
, I receive an instance of component already. I can probably use this
in callback as you mentioned above:function renderSomething(instance, container) => {
return new Promise((resolve, reject) => {
try {
ReactDOM.render(instance, container, function () {
resolve(this);
});
} catch (e) {
reject(e);
}
});
}
renderSomething(<Foo />, container).then(() => { ... });
How does one reference the component in such scenario? I need it for tests and I'd like to avoid extra boilerplate.
this
in callback is such a gem, it does not work with arrow functions though.
we have eslint rules in place that throw warnings when it's used in such fashion (since it's not future proof)
We are not enforcing such rules in FB codebase. It may be a long time before we actually deprecate the return value, if we ever do. Are you sure it is reasonable to keep this lint rule enabled in your codebase, if it鈥檚 causing you troubles?
I don鈥檛 think we鈥檒l come up with a more convenient API until we actually decide to deprecate the return value (again, if we ever do).
How does one reference the component in such scenario? I need it for tests and I'd like to avoid extra boilerplate.
I would just recommend to keep using return value. Even if we eventually deprecate it for product code, I鈥檓 pretty sure we鈥檒l keep it around for tests because they should always run synchronously.
Maybe you could turn off the lint rule just for tests?
@gaearon I see. I had impression that this is critical. Also eslint-plugin-react
enabled it by default which seems overly pushing to move away from using ReactDOM.render
in such fashion. Anyway, I am going to disable a corresponding lint rule for now and move on. Thanks again!
I think there was a time when we thought it would go away sooner. But for now we鈥檙e sticking with it (even if slightly discouraging it in product code).
Hmm. Seems like I spoke too soon 馃槥
https://github.com/facebook/react/issues/10309#issuecomment-318701037
In your case it seems like this would be the best solution:
let component;
ReactDOM.render(<Foo ref={c => component = c} />, container, () => {
// Do something with component
});
I am not able to pass ref, I receive an instance of component already. I can probably use this in callback as you mentioned above:
Minor technical correction: you receive an element, not an instance. The instance is what you want to get access to.
If you receive an element, you can clone it with a different ref.
function cloneWithRef(element, newRef) {
const oldRef = element.ref;
return React.cloneElement(element, {
ref(value) {
if (typeof oldRef === 'function') {
oldRef(value);
}
newRef(value);
}
});
}
const originalElement = <Foo />; // could be an argument
const element = cloneWithRef(originalElement, inst => {
if (inst) {
// Do something with component
}
});
ReactDOM.render(element, container);
Can you tell more about how you use an instance in tests? Usually tests are done on a wrapper, e.g. Enzyme鈥檚 shallow()
. Are you calling methods on it?
I have a UI component that uses document
events so I need to plug it into DOM for testing.
// setup
let container;
function renderIntoDocument(element) {
container = document.createElement('div');
document.documentElement.appendChild(container);
return ReactDOM.render(element, container);
}
// teardown
afterEach(() => {
if(container) {
ReactDOM.unmountComponentAtNode(container);
container = null;
}
});
So then I do something like that:
const component = renderIntoDocument(
<MyComponent isOn={ false } onChange={ onChange } />
);
const domNode = ReactTestUtils.findRenderedDOMComponentWithTag(component, 'input');
// document.dispatchEvent(new MouseEvent( ... ));
I could actually pass props and assume that Component
does not change, then accessing ref
would be easy without need to clone with new ref.
Hmm. Can you not find it via document.getElementsByTagName('input')
? What鈥檚 the advantage of doing this via React?
@gaearon actually I can, I never thought of that. But given that rendering may be asynchronous, the best way to go is to add ref
and use ReactDOM.render
callback to be notified when element is rendered. Then I can do any kind of lookup, either using DOM query or directly via ReactTestUtils.findRenderedDOMComponentWithTag(component, '..')
@gaearon thanks for helping me with this. I consider this solved given that in worse case I can always clone element with new ref 馃憤 But really, the easiest is to render my component inside of render function and simply pass props as arguments, then there is no need to override ref via cloning or whatsoever, i.e:
let container;
function renderIntoDocument(props) {
container = document.createElement('div');
document.documentElement.appendChild(container);
return new Promise(function (resolve, reject) {
let ref;
try {
// since I actually render the same component all the time I can hardcode it here `MyComponent`
ReactDOM.render(<MyComponent {...props} ref={ (e) => ref = e; } />, container, () => {
resolve(ref);
});
} catch (e) {
reject(e);
}
});
}
But given that rendering may be asynchronous, the best way to go is to add ref and use ReactDOM.render callback to be notified when element is rendered
Yep.
Most helpful comment
Hmm. Seems like I spoke too soon 馃槥
https://github.com/facebook/react/issues/10309#issuecomment-318701037
In your case it seems like this would be the best solution:
Minor technical correction: you receive an element, not an instance. The instance is what you want to get access to.
If you receive an element, you can clone it with a different ref.