I was working on a component that has a button which toggles a boolean. This boolean is supposed to determine if a child component in the HTML need to re-render or not, since I want the ngOnInit function in the child to be re-run.
The situation is described in the app component here: https://codesandbox.io/s/angular-qxtm8
The app.component is the parent and second.component is the child.
I have tried three different solutions. They are onTestClickOne, onTestClickTwo, and onTestClickThree in app.component.ts. onTestClickOne and onTestClickTwo successfully re-triggers the ngOnInit in the child component. We can see the console log in it is printed on the console whenever I click the corresponding buttons. However, onTestClickThree didn't work.
I'm not 100% sure why onTestClickThree didn't work, and onTestClickTwo did.
My guesses are the following:
onTestClickTwo works because the change detection in Angular is run after the event handler has been executed. So, it will detect the boolean has been set to true. After that, the event loop will get the callback of the setTimeout and put it into the stack. Angular will execute change detection after finishing the callback.
onTestClickThree didn't work because, by the time Angular runs change detection, the boolean is already true. Angular doesn't know that it has been changed.
Let's tackle the main issue there, which is your design : why would you re-render the component to trigger ngOnInit again ?
Sure, in the case of your example, that's no big deal. But what happens for a fully coded component, making http calls, having children and all ? That will cause some severe performance issues.
Instead of re-rendering the component, you should use a function to do that.
If the event (that is initially supposed to re-render the component) comes from the child, then use an #Output. If it comes from the parent, use a #ViewChild reference.
As you can see it works well, without any detection issue.
Related
See the following Stackblitz: https://stackblitz.com/edit/angular-psqzbo?file=src%2Fapp%2Fhello.component.ts
Notice that there are two bindings to the width member: one is in the template, and the other is a host binding. The host binding is commented out right now. Notice that no ExpressionChanged error is being thrown-- this is because this.cdr.detectChanges() is being called after we update width in ngAfterViewInit.
Now uncomment the host binding. Observe that an ExpressionChanged error is thrown. Why? What makes these bindings different? Is this a bug?
EDIT: This is not a dupe of the linked question. I know why detectChanges is needed here, my question is why it is not working on the host binding. Please re-read.
This error occurs for you because you are making a change that invalidates the previously rendered component view.
Quote from this documentation
Angular's unidirectional data flow rule forbids updates to the view after it has been composed. Both of these hooks fire after the component's view has been composed.
What this means for your situation:
When Angular first renders/composes the view of this component (prior ngAfterViewInit) the width of the element is set to an initial value. This code changes the width of an element, which causes a change to the view.
I think this example using background color makes this more obvious. In the first pass rendering the view, the color is red (and you can briefly see this on the screen). In your situation, the width binding is undefined on the first pass through.
Then, the ngAfterViewInit causes a change that makes the previously created view invalid, changing the width or the color, triggering the error. I translate this error as Angular saying "I did a bunch of work and made a view that was perfect, then you did something that made that work worthless. You shouldn't do that, because it interrupts some performance optimizations/assumptions I have".
This can be fixed by ensuring the component change happens after the ngAfterViewInit method has finished running, by using setTimeout. Fixed example Or by ensuring the component change happens before the view is rendered by moving it to ngOnInit.
You may notice that I do not have detectChanges in the examples I created. This is intentional, as that is a red herring and actually has no relation to the problem (though you correctly state this is necessary for the template binding to work). When the host binding is commented out in your example, there is no expression problem because this.width has no impact on the rendered view of the component.
There is no issue when the binding is in the template because this is causing changes to the content of the component - not the component's own view.
Speculation / Things I don't fully understand:
I believe this behavior boils down to changes to the Shadow DOM vs Light DOM (I'm making guesses based on info from this SO question). At the time ngAfterViewInit is running, I believe there is already an empty tag <my-component></my-component> in the Light DOM (IE actually on the page). Changing the child content does not cause an issue, because at this point it doesn't actually exist on the page and is part of the Shadow DOM. However, changing a host binding triggers a change to the real elements on the page (part of the Light DOM) - hence the error.
I have a large array that I am using in a component (component A) with *ngFor with a nested *ngFor.
Component B initialises a jquery plugin which registers a document mousemove event handler, I am using this.zone.runOutsideAngular to init the plugin and I am calling this.ref.detectChanges() in the callback as I need to update the UI on mousemove inside the component B.
Component A is not a child of component B.
As soon as the component A is rendered change detection becomes very slow. the array does not change and I am using the ChangeDetectionStrategy.OnPush strategy for component A but when I fire ref.detectChanges() inside component B, ngDoCheck gets called on component A and I can see a noticeable jank on mousemove.
Is there a way to tell angular to completely ignore the large array of items in component A and allow me to handle when the UI should be updated? I thought that using ChangeDetectionStrategy.OnPush would give me what I need but I have tried removing all #Input()s from component A and anytime I call this.ref.detectChanges() inside component B it is still firing ngDoCheck and it is obvious that this is very slow.
I can scroll through the list of items no issue, but it is when I am triggering the detectChanges inside the mousemove on component B that is causing the issue. I know I could manually update the DOM but I think this would just be a workaround as it would only address the jank on mousemove and not the issue around the change detection being slow.
I have got to the bottom of this issue.
The problem was that inside component A for the nested *ngFor I was using a child component to render each sub item which meant that although I was using the ChangeDetectionStrategy.OnPush strategy, it still required a ref check for each item.
I have now moved the html from the child component into component A directly and this has had a huge impact on performance.
this.ref.detach() to remove the detector from from the tree completely, that should stop the checking. Then you can still call detectChanges to do it manually, and reattach to bring it back online.
Maybe also debouncing the mousemoves (rxjs debounceTime()) might help, unless you really need to track every mousemove?
One more optimization if you already didn't, add trackBy: yourTrackByFn to the ngFor(s).
After digging into the code a bit I see that invalidate() on a control will increase a counter which seems to mark the control as invalidated.
This seems to lead to a rerender.
So if you have a control that you want rerendered, is it better practice to use invalidate() or rerender()?
How does a rerender actually get triggered? (other than by explicitly invoking it of course)
Both are marked as "protected", meaning you should not call any of them unless you are really deep into developing custom controls.
A control gets invalidated when (for example) a property is changed. In that case you usually want to re-render the control, that's why setters call invalidate by default.
When you overwrite your invalidate method in your custom control, you can analyze the source of the invalidation and then decide whether you really want to rerender etc. See for example the unified.Shell which decides what to do on invalidate based on the source:
https://sapui5.netweaver.ondemand.com/sdk/resources/sap/ui/unified/Shell-dbg.js line 1539ff
Again: Protected means that it should not be called from the outside (and that it should not be necessary, except for debugging).
Is it better practice to use invalidate() or rerender()?
As an application developer
Neither invalidate() nor rerender() should be used as they're not public APIs.
As a control developer
Usually, the framework already manages rerendering of the controls automatically if one of the control settings (properties, aggregations, or associations) changes. But if it's still required to trigger the rerendering explicitly, the API invalidate() should be favored over rerender() because:
rerender()
renders the control synchronously (blocking the main/UI thread)
works only when the control has been rendered before (no initial rendering possible)
does not combine multiple state changes into a single rerendering
causes additional layout trashing
has become deprecated since 1.70.
invalidate(), on the other hand, just adds the control to a collection of to-be-rendered controls so that it can be rendered together with them in one process asynchronously (currently in the next browser task using setTimeout(fn, 0)).
I encountered such thing in my work recently. I am fixing an animation issue but the bad thing is I have to start the animation after rendering. If that is all I can handle it, but the worse thing is the control rendering twice. When the first rendering happen, the animation start to do, but the second rendering comes immediately and the DOM was restructed, then the animation is terminated and the effect is like no animation executed. After investigate the code, I found there is a place invoke render() method which will execute render immediately. After change it to invoke invalidated() method, it looks the animation works well. It looks multiple invalidate() method invocation only cause executing render once.
I have just started using React, and a couple of times I have thought to myself: "Why is there no componentDidRender event?".
Say that I have a component that renders a table to the DOM, and I want to use bootstrap-sortable on this table to allow the user to sort on whatever column he wants. In the case of bootstrap-sortable you need to run $.boostrapSortable() after the table is drawn, in order to initialize the plugin.
As I see it, there are two handlers on a React component that would be logical to consider to use for this purpose:
componentDidMount: This does not work because the DOM does not seem to be updated at this point of the execution.
componentDidUpdate: This could possibly work, but it does not fire on the initial render.
I am not saying that React is actually missing a componentDidRender function, because I assume that there is a perfectly logical explanation as to why it is not there. I am just asking if someone could explain why such a function is not present, and what would be the "React way" to handle a case like the one above.
In componentDidMount you can do: this.getDOMNode() to get a reference to the underlying DOM for that component. So if you do want to use your mounted component with jQuery you can do:
componentDidMount: function() {
$(this.getDOMNode());
}
http://facebook.github.io/react/docs/working-with-the-browser.html
Here's a fiddle which shows jQuery acting on the DOM node of a react component:
http://jsfiddle.net/sa5e88ys/1/
As you can see, it adds a border to the div as expected. If you're still having problems I guess it could be with the plugin you're using rather than jQuery or react?
Although there's no componentDidRender, you can make a method with the desired behavior and call it in both componentDidMount (which is only called after the first render) and componentDidUpdate (which is called after every render but the first).
Also, this is the preferred way of getting a ref to a DOM node from within the component:
https://facebook.github.io/react/docs/refs-and-the-dom.html
I am using things like:
var MUSIC = React.renderComponent( Music({ }), document.getElementById("music-div"))
to later in the script, in an independent element (so not parent of MUSIC) do:
MUSIC.setProps({ url: 'http://...' })
to send a song to de music player, which is detached from the rest, so it does not accidentally gets refreshed by react, because it was programmatically generated (wavesurferjs)
Fine, however, the recent few updates (.11.x) have apparently deprecated that. I do understand where they are coming from, it fits the whole React logic.
However, how will we now ever programmatically modify state/props from outside? Even when I want to talk directly to the parent, which should be allowed.
The changelog tells me in this case the MUSIC variable would have become a descriptor, however, in consoles out the exact same object as far as I can tell. And the documentation says nothing about this descriptor and even less about alternative possibilities.
(http://facebook.github.io/react/blog/2014/07/17/react-v0.11.html#descriptors)
So, if I have two divs
<div id="main-div"> <button></button></div>
<div id="music-div"> </div>
And want to keep them separate, how would I go about giving two parallel parents each-other props?
I don't want to put both in one react div, which would not even solve my problem, because, how would the button in main-div give the props to music-div?
Or would their conceived alternative be to just create a new instance on that id and hope it diffs to 0?
The update states:
"You could store that reference and then call functions on it (eg
component.setProps(...)). This no longer works."
However, that still does work, with (0.11.1) so I don't understand what they are talking about?
You've got several options.
Wrap both main-div and music-div with an "Application" component. Pass a handler down that changes its state so the div's are re-rendered.
Use an event bus to dispatch and listen to events. Basically a component exposes its private setState/setProps() methods to the event bus in a listener. The other component dispatches an event that triggers that listener.