I'm trying to understand when componentWillUnmount is called. I have this class-based component:
class ClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = { date: new Date().toLocaleString() };
}
componentDidMount() {
console.log('mounted');
}
componentWillUnmount() {
console.log('unmounting');
}
render() {
return <div></div>;
}
}
export default ClassComponent;
But when I check the console, it already prints "unmounting" although the component is still mounted. In total I have three logs in the console:
mounted
unmounting
mounted
Can someone explain to me why that is the case?
Found the reason, it is due to the new strict-mode behaviors: https://reactjs.org/blog/2022/03/29/react-v18.html#new-strict-mode-behaviors
Related
I'm not sure where is the bug, maybe I'm using rxjs in a wrong way. ngDestroy is not working to unsubscribe observables in NativeScript if you want to close and back to your app. I tried to work with takeUntil, but with the same results. If the user close/open the app many times, it can cause a memory leak (if I understand the mobile environment correctly). Any ideas? This code below it's only a demo. I need to use users$ in many places in my app.
Tested with Android sdk emulator and on real device.
AppComponent
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Subscription, Observable } from 'rxjs';
import { AppService } from './app.service';
import { AuthenticationService } from './authentication.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnDestroy, OnInit {
public user$: Observable<any>;
private subscriptions: Subscription[] = [];
constructor(private appService: AppService, private authenticationService: AuthenticationService) {}
public ngOnInit(): void {
this.user$ = this.authenticationService.user$;
this.subscriptions.push(
this.authenticationService.user$.subscribe((user: any) => {
console.log('user', !!user);
})
);
}
public ngOnDestroy(): void {
if (this.subscriptions) {
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
}
}
async signIn() {
await this.appService.signIn();
}
async signOut() {
await this.appService.signOut();
}
}
AuthenticationService
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { AppService } from './app.service';
#Injectable({
providedIn: 'root',
})
export class AuthenticationService {
public user$: Observable<any>;
constructor(private appService: AppService) {
this.user$ = this.appService.authState().pipe(shareReplay(1)); // I'm using this.users$ in many places in my app, so I need to use sharereplay
}
}
AppService
import { Injectable, NgZone } from '#angular/core';
import { addAuthStateListener, login, LoginType, logout, User } from 'nativescript-plugin-firebase';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
const user$ = new BehaviorSubject<User>(null);
#Injectable({
providedIn: 'root',
})
export class AppService {
constructor(private ngZone: NgZone) {
addAuthStateListener({
onAuthStateChanged: ({ user }) => {
this.ngZone.run(() => {
user$.next(user);
});
},
});
}
public authState(): Observable<User> {
return user$.asObservable().pipe(distinctUntilChanged());
}
async signIn() {
return await login({ type: LoginType.PASSWORD, passwordOptions: { email: 'xxx', password: 'xxx' } }).catch(
(error: string) => {
throw {
message: error,
};
}
);
}
signOut() {
logout();
}
}
ngOnDestroy is called whenever a component is destroyed (following regular Angular workflow). If you have navigated forward in your app, previous views would still exist and would be unlikely to be destroyed.
If you are seeing multiple ngOnInit without any ngOnDestroy, then you have instantiated multiple components through some navigation, unrelated to your subscriptions. You should not expect the same instance of your component to be reused once ngOnDestroy has been called, so having a push to a Subscription[] array will only ever have one object.
If you are terminating the app (i.e. force quit swipe away), the whole JavaScript context is thrown out and memory is cleaned up. You won't run the risk of leaking outside of your app's context.
Incidentally, you're complicating your subscription tracking (and not just in the way that I described above about only ever having one pushed). A Subscription is an object that can have other Subscription objects attached for termination at the same time.
const subscription: Subscription = new Subscription();
subscription.add(interval(100).subscribe((n: number) => console.log(`first sub`));
subscription.add(interval(200).subscribe((n: number) => console.log(`second sub`));
subscription.add(interval(300).subscribe((n: number) => console.log(`third sub`));
timer(5000).subscribe(() => subscription.unsubscribe()); // terminates all added subscriptions
Be careful to add the subscribe call directly in .add and not with a closure. Annoyingly, this is exactly the same function call to make when you want to add a completion block to your subscription, passing a block instead:
subscription.add(() => console.log(`everybody's done.`));
One way to detect when the view comes from the background is to set callbacks on the router outlet (in angular will be)
<page-router-outlet
(loaded)="outletLoaded($event)"
(unloaded)="outletUnLoaded($event)"></page-router-outlet>
Then you cn use outletLoaded(args: EventData) {} to initialise your code
respectively outletUnLoaded to destroy your subscriptions.
This is helpful in cases where you have access to the router outlet (in App Component for instance)
In case when you are somewhere inside the navigation tree you can listen for suspend event
Application.on(Application.suspendEvent, (data: EventData) => {
this.backFromBackground = true;
});
Then when opening the app if the flag is true it will give you a hint that you are coming from the background rather than opening for the first time.
It works pretty well for me.
Hope that help you as well.
How do we update component data, inside a app event? this.matches = x gets ignored
import * as app from "tns-core-modules/application";
export class HomeComponent implements OnInit {
matches; // How to refresh this??
constructor() {
app.on(app.resumeEvent, (args: app.ApplicationEventData) => {
// how to change matches here??
});
}
}
You have to run your code inside NgZone as resume event will be triggered outside Angular's context.
constructor(ngZone: NgZone) {
app.on(app.resumeEvent, (args: app.ApplicationEventData) => {
ngZone.run(() => {
// Update here
});
});
}
I'm using the fetch api to request data from my rails controller. The request getting to the controller and I'm successfully looking up some data from my database.
def show
set_recipe
#recipe.to_json
end
However, the fetch statement in my react component isn't setting the state object.
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {
data: null,
};
}
componentDidMount() {
fetch('/recipes/1')
.then(response => response.json())
.then(data => this.setState({ data }));
}
render() {
return(<div>{this.state.data}</div>)
}
}
Thoughts?
UPDATE: Using postman I realized that my component wasn't actually returning any json. I updated the controller code above to look like what you see below. After this postman started getting json responses. However, this didn't fix the fact that my component state was still null. I did some debugging in the chrome console and with the fetch was able to get a json from the server. This narrows it down to being an issue with how I'm using fetch inside the componentDidMount.
def show
set_recipe
render(json: #recipe)
end
UPDATE 2: RESOLVED
Got it working. I downloaded the react developer extension for chrome and I could see that this.state.data was actually getting set. But I was getting an error saying that react couldn't render an object. So I needed to add the .name to get the name string out of the json object. Then finally I was getting an error because this.state.data was initialized as a null and I guess the first time the component renders before mounting it hadn't yet set it to json and .name isn't a method that could be called from null. By initializing it to an empty string it worked, not sure why though. Below is the final component:
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {
recipe: ''
};
}
componentDidMount() {
fetch('/recipes/1')
.then(response => response.json())
.then(recipe => this.setState({ recipe }));
}
render() {
return(<div>{this.state.recipe.name}</div>)
}
}
Here is what it needed to look like. See original post for more detail.
Controller:
def show
set_recipe
render(json: #recipe)
end
Component:
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = {
recipe: ''
};
}
componentDidMount() {
fetch('/recipes/1')
.then(response => response.json())
.then(recipe => this.setState({ recipe }));
}
render() {
return(<div>{this.state.recipe.name}</div>)
}
}
I'm trying to implement a custom select which displays a list of languages fetched from an API.
I make the api call in the componentDidMount lifecycle hook and then update the state of my component according to the data I fetched.
Everything seems to work, yet I always get this error:
Warning: setState(...): Can only update a mounted or mounting
component. This usually means you called setState() on an unmounted
component. This is a no-op. Please check the code for the
LanguageSelect component.
Here's a snippet of my code:
export default class LanguageSelect extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: false,
languages: []
};
}
// ...
componentDidMount() {
http.get('/api/parameters/languages')
.then(
// the Array.map is just to restructure the data fetched form the API
data => this.setState({ languages : data.map(l => ({ label: l.LocalName, value: l.Code, })) }),
// Error case
error => console.error('Error: Languages data could not be fetched', error)
)
}
// Render function
}
I don't understand, The only setState call I make is inside a componentDidMount() lifecycle thus is only executed when the component is mounted ?
Thanks in advance
You could use isMounted() to check if it's mounted
if (this.isMounted()) {
this.setState({...});
}
Although it's an anti pattern and you can find some proposals on best practice solutions here: https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
I've got a fairly simple react container component that attempts to call set state in an ajax callback called from componentDidMount. The full error is:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the UserListContainer component.
the order of operations from my console.log are:
render
child-render
componentDidMount
ajax-data
[Big ol Error Message]
I started out using async/await but when I received the error I went back to callbacks with the same result. This is the relevant code:
export class UserListContainer extends React.Component<any, any>
{
constructor() {
super();
this.state = {
users: [], request: {}
};
}
//componentDidMount = async () => {
componentWillMount = () => {
console.log('componentWillMount');
//var response: Models.IUserQueryResponse = await Api.UserList.get(this.state.request);
Api.UserList.get(this.state.request).then((response) => {
console.log('ajax-data');
if (response.isOk) {
this.setState({ users: response.data, request: response.state });
}
});
}
render() {
console.log('render');
return <UserList
request={this.state.request}
users={this.state.users}
onEditClick={this.edit}
onRefresh={this.refresh}
/>;
}
Any help would be appreciated.
you cannot set state in componentWillMount because your component could be in a transitioning state.. also it will not trigger a re-rendering. Either use componentWillReceiveProps or componentDidUpdate.
Now that aside your issue is that you are calling setState in the callback from an API request. and the issue with that is you probably have unmounted that component and dont want to setState anymore.
you can fix this with a simple flag
constructor() {
super();
this.state = {
users: [], request: {}
};
this.isMounted = false;
}
componentDidMount(){
this.isMounted = true
}
componentWillUnmount(){
this.isMounted = false;
}
then in your api request you would do this.
Api.UserList.get(this.state.request).then((response) => {
console.log('ajax-data');
if (response.isOk && this.isMounted) {
this.setState({ users: response.data, request: response.state });
}
});
I think is better to use componentWillMount() instead of componentDidMount() cause you want to load the list and then set the state, not after the component was mounted.