React Create Portal event listening - reactjs

So I am trying to migrate an existing Portal implementation from the old unstable_renderSubtreeIntoContainer to the new portal implementation.
I have an issue though, the relevant code has the following functionality:
unstable_renderSubtreeIntoContainer(
this,
this.props.children,
this.portalElement,
() => {
if (this.props.isOpen) {
this.props.onRender(this.portalElement,
this.getTargetElement());
}
callback(); //runs this.props.open() if the update ran open
},
);
Some of the open/close logic could be simplified by wrapping the component to be rendered inside an object and the appropriate callbacks could be called from there. But it seems createPortal has no callback to allow you specify when a render has or hasn't taken place. Is there anyway to synchronously or asynchronously on a createPortal call has finished rendering?

Related

React: accessing internal operation queue of React

React collects operations(like DOM operations such 'ADD','REPLACE','REMOVE' and more) so they could execute them in batch in one shot at the end of each render.
for example, setState call inside React component is scheduled to the end of the construction of this React tree by adding this operation to the operation queue. then react will go over this queue and will decide what changes need to be made to the DOM.
React will decide whether or not to call another render based on whether or not the operation queue is empty or not.
more info on this awesome tutorial which summarizes the basics of how React works internally.
I need access to this queue to decide whether or not the current render was the last for a very custom React component. (YES maybe I can avoid it but currently, this is my requirement)
the access must be from inside of this component.
my check will be called from the lastest useEffect which is after the render ends and the DOM was updated and is the latest lifecycle event, so if the operation queue is empty there will be no more renders for sure. (here nice article explains and demonstrates the order of hook calls)
couldn't find any public API, but a workaround would also be acceptable. (forking and editing React is not a workaround)
this src file is probably the main logic to this queue. and this is the type of the actual queue. however this is the source code and this queue is not exported in the built versions of react(neither development or production build)
so, is there a way to access the internal operation queue of React?
EDIT - Warning
this is for educational purposes only - do not use it on production!
this approach is not safe based on React core team member - I've already asked. it could be safe if you plan to use a fixed version of React without upgrading later.
####### END OF EDIT #######
SOLVED!
so after many many hours digging into React codebase I finally wrote a hook that tells if any update is currently scheduled.
Note: would work for function components only, and this hook is not well tested.
you can see some internal state of React by the undocumented __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED property. this prop holds ReactCurrentOwner which is basically a reference to the current component that is being constructed.
const currentOwner = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner?.current;
currentOwner is the current component that is being constructed. this prop is available during renders only(because in effects is after render no component is being currently constructed).
but because another render can be triggered from state set from effects we should always call ower check from the latest effect
inside it .current.memoizedProps you will find a linked list of all hooks that were declared to this point on this component.
each hook holds a queue for holding scheduled updates, and inside it, there is a pending prop that tells if any update is currently scheduled for the next render.
we could run over this linked list to find out if an update is scheduled by any hook:
const wouldUpdate = (currentOwner) => {
let newObj = currentOwner?.memoizedState;
// go over the linked list of hooks and find out if there is any pending update
while (newObj && 'next' in newObj) {
newObj = newObj['next'];
if (newObj?.queue?.pending) return true;
}
return false;
};
so to summer up we could build a custom hook to check if the current render is the latest scheduled render:
const wouldUpdate = (currentOwner) => {
let newObj = currentOwner?.memoizedState;
// go over the linked list of hooks and find out if there is any pending update
while (newObj && 'next' in newObj) {
newObj = newObj['next'];
if (newObj?.queue?.pending) return true;
}
return false;
};
export const useDoesUpdateIsScheduled = () => {
// #ts-ignore
// hold the current owner ref so we could call it from effects
const currentOwner = useRef(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner?.current);
return () => wouldUpdate(currentOwner.current);
};
so many hours for so little code... ;<
Usage:
const YourComponent = (props) => {
//..
// code, hooks ,logic, effects would be here
//..
// should be could from the last useEffect
const wouldUpdate = useDoesUpdateIsScheduled();
useEffect(() => {
console.log(wouldUpdate());
});
return <div>... your jsx here ...</div>;
};
screenshot of test component on mount:
you can see that on the latest render our hook tells us there are no pending updates.
you can also call wouldUpdate from function body but take into account that updates can be scheduled from effects (means that calling it while rendering would not catch these updates)
the popular why-did-you-render also uses this undocumented __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED property to achieve it's goal.
THATS IT(actually it wasn't worth the hours, and it will probably break on various cases as this is not public API).

Test Drop Down With React Testing Library

I am having trouble testing a drop down populated with data from an API call in React Testing Library. Below is a CodeSandbox showing the issue
https://codesandbox.io/s/mutable-sea-wtt9u
If I change App to use a hardcoded array to populate the drop down (commented out in App component), the test passes.
Thanks
When your data comes from an asynchronous fetch call, the DOM doesn't get updated synchronously, and you have to use one of the async utilities to wait for the update. This works in your case (tested in your Codesandbox):
// import `wait` directly from React Testing Library
import { render, wait } from '#testing-library/react';
...
await wait(() => {
fireEvent.change(selectElement, { target: { value: "1" } });
expect(selectElement.value).toBe("1");
});
Here's the React Testing Library docs on async utilities: https://testing-library.com/docs/dom-testing-library/api-async
EDIT: It looks like you might have changed your CodeSandbox code. Now you need to wait for the async call to be made before firing the event, since you're fetching data on mount. I've updated my answer and made sure tests pass on your current CodeSandbox.
You need to mock your fetch events. I wrote an article on how to do that. You can find it here.

Test fails because AngularJS has not initialized in time

I'm trying out TestCafe for an AngularJS (v1.6) application.
I have a button then when clicked, opens a modal (from UI bootstrap). This works fine when I try myself in Chrome.
<button class="btn" ng-click="open()">Open</button>
Our application requires user authentication, and the login page is not Angular-based. That phase of my test works fine.
However, when the actual test runs, it "clicks" the button but nothing happens.
I suspect, but can't prove, that it's clicked before AngularJS has properly initialized on the page.
With some research, I found the testcafe-angular-selectors project and a waitForAngular method but that appears to apply only to Angular2+.
import { Role, Selector } from 'testcafe';
const regularAccUser = Role('http://127.0.0.1:8080', async t => {
await t
.typeText('[name=username]', 'abc')
.typeText('[name=password]', '123')
.click('.btn-primary');
});
fixture`Characters Modal`;
test('modal title', async t => {
await t
.useRole(regularAccUser)
.navigateTo('http://127.0.0.1:8080/fake/page')
.click('.btn')
.expect(Selector('.modal-title').innerText).eql('Insert Symbol');
});
Adding .wait(1000) before the click solves the issue. It's not waiting for Angular to load. I'd rather not have waits in every test - is there some other technique I can use?
You can use TestCafe assertions as a mechanism to wait until an element is ready before acting on it.
A typical waiting mechanism would be:
const button = Selector('button.btn')
.with({visibilityCheck: true});
await t
.expect(button.exists) // wait until component is mounted in DOM
.ok({timeout: 10000}) // wait enough time
.hover(button) // move TestCafe cursor over the component
.expect(button.hasAttribute('disabled'))
.notOk({timeout: 10000}) // wait until the button is enabled
.click(button); // now we are sure the button is there and is clickable
This article may also help you in managing all those waiting mechanisms.
As you correctly mentioned, the waitForAngular method is intended for Angular only, not for AngularJS.
I recommend you create your own waitForAngularJS function and call it on the beforeEach hook and after the role was initialized.
In the simplest case, it can be implemented as follows:
function waitForAngularJS (t) {
await t.wait(1000);
}
fixture `App tests`
.page('page with angularjs')
.beforeEach(async t => {
await waitForAngularJS(t);
});
However, the use of the wait method is not a solid solution. I recommend you find a way to detect if AngularJS is loaded on a page on the client side. If it is possible, you can implement the waitForAngularJS method using the TestCafe ClientFunctions mechanism.
This post can be useful as well: How to check if angular is loaded correctly

Is it necessary to call `unmountComponentAtNode` if the component's container is removed?

I render a React component SettingsTab within a wrapper called TeamView. Its API looks something like
class TeamView {
constructor() {
this.el = document.createElement('div');
}
render() {
ReactDOM.render(<SettingsTab/>, this.el);
return this;
}
remove() {
this.el.remove();
}
}
used something like
// to present the team view
const teamView = new TeamView();
document.body.appendChild(teamView.render().el);
// to remove the team view
teamView.remove();
And what I'm wondering is, should TeamView#remove call ReactDOM. unmountComponentAtNode(this.el) before calling this.el.remove()?
The examples I can find around the web make it seem like unmountComponentAtNode only needs to be called if the container is going to remain in the DOM; and the new portals example just removes the container, without calling unmountComponentAtNode.
But, I'm not sure if that's special because it's using a portal, and this post makes it kind of seem like it's always good practice to call unmountComponentAtNode.
Yes, it is important to call unmountComponentAtNode() because if you don't do this, none of the components below in the tree will know they have been unmounted.
User-defined components often do something in componentDidMount that creates a reference to the tree from the global environment. For example, you may add a window event handler (which isn't managed by React), a Redux store subscription, a setInterval call, etc. All of this is fine and normal as long as these bindings are removed in componentWillUnmount.
However, if you just remove the root from the DOM but never call unmountComponentAtNode, React will have no idea the components in that tree need to be unmounted. Since their componentWillUnmount never fires, those subscriptions stay, and prevent the whole tree from getting garbage collected.
So for all practical purposes you should always unmount the root if you're going to remove that container node. Otherwise you'll most likely get a memory leak—if not now, then later when some of your components (potentially deep in the tree, maybe even from third-party libraries) add subscriptions in their componentDidMount.
Even though you called this.el.remove(), you should still call the unmountComponentAtNode(this.el) because unmountComponentAtNode will clean up its event handlers and state, but the remove method will not.
For example, Eventhough you have clicked to remove the div, you can still call it's click event handlers:
var tap = document.querySelector('.tap');
var other = document.querySelector('.other');
tap.addEventListener('click', function(e) {
console.log(tap.getAttribute('data-name') + ' has been clicked');
tap.remove();
});
other.addEventListener('click', function(e) {
tap.click();
});
<div class="tap" data-name="tap">First Click me to remove me</div>
<div class="other">Then Click me </div>
I asked this question in the #react-internals Discord channel and received the following response:
So, this tallies with what #jiangangxiong says above: as long as we
don't keep our own references to component DOM elements
nor attach event handlers outside of React
and only need to support modern browsers
we should only need to remove the container to have the component's event handlers and state garbage collected, no need to call unmountComponentAtNode.

Binding to event handler that calls setState in ComponentDidMount produces warning

I'm using jQuery to create event bindings in a ReactJS component's componentDidMount function, which seems like the right place to do this.
$('body').on('defaultSearchContext.registerQueryEditor', (function(_this) {
return function(event, component) {
_this.setState({
queryEditors: _this.state.queryEditors.concat([component])
});
};
})(this));
This code isn't actually run on componentDidMount, it's simply setting up the binding that later calls setState when the event fires. However, this generates the following warning every time this event triggers, which pollutes my console with dozens of warnings:
Warning: setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
I have tried moving the setState code to a separate function like onEvent and calling that from the binding in componentDidMount but the warning is still produced.
Ideally, I'd like to create the binding in the proper place, indeed, there is some issue with doing it in componentDidMount. If not, I'd like to know if it's possible to silence the warning, or whether I should perhaps file a bug for ReactJS itself. If it helps, I'm using ReactJS 0.14.3 (latest at this time).
This is similar to, but not the same as React Js onClick inside render. In that case, the solution is to return an anonymous function to onClick, but that doesn't seem applicable to my situation.
You are trying to coordinate events between independent components. This is a fairly standard thing to do, and it doesn't require DOM events. The standard practice for doing this in React is to use a store/dispatcher pattern like Redux or Flux (I personally prefer redux). However, if this is part of a larger, not-completely-React application, then this may not be possible. If it is just for a small piece of an React app, it may still be overkill.
All you need is an object to coordinate events. An event is just a collection of callbacks, possibly typed or keyed. This requires nothing more than an object shared between two places. DOM Events are overkill; jQuery is overkill. You just need to trigger a callback.
This is a VERY SIMPLE event coordinator.
let simpleEventCoordinator = {
callbacks: new Map(),
getHandler(eventKey) {
let handler = this.callbacks.get(eventKey);
if (!handler) {
handler = new Set();
this.callbacks.set(eventKey, handler);
}
return handler;
},
registerCallback(eventKey, callback) {
this.getHandler(eventKey).add(callback);
},
removeCallback(eventKey, callback) {
this.getHandler(eventKey).delete(callback);
},
trigger(eventKey, data) {
this.getHandler(eventKey).forEach(c => c(data));
}
Keep a map of callbacks, which will be nameOfEvent => callback(). Call them when asked. Pretty straightforward.
I know nothing about how your components are structured, but you said they are independent. Let's say they look like this:
React.render((
<div>
<QueryManager />
<button onClick={() => simpleEvent.trigger('event')}>{'Update'}</button>
</div>
), document.body);
This is all your component needs to handle this event
componentDidMount() {
simpleEvent.registerCallback('event', this.update);
}
componentWillUnmount() {
simpleEvent.removeCallback('event', this.update);
}
update() {
//do some stuff
}
I've put together a very simple codepen demonstrating this.
Looking at the source code of where that warning is coming from, it appears that if some reference is maintained before an update is about to happen, it throws that warning. So maybe the way your mixing the jQuery events and react is creating a memory leak? Its hard to say exactly because of the lack of surrounding code to your snippet what else could be going on.

Resources