React Class Runs Code Excessively and Does Not Update State - javascript

In the constructor, I'm running this.onLoadAccountRetrieve(); to use the API to get the account details, and if an account is returned the use setState() to update the account and isLoginActive variables.
// outside of class
export interface PageState {
account: AccountDTO;
anchorEl: HTMLElement | null;
isLoginActive: boolean;
isMenuOpen: boolean;
}
// inside class
constructor(props: PageProps) {
super(props);
this.state = {account: null,anchorEl: null,isLoginActive: false,isMenuOpen: false};
this.onLoadAccountRetrieve();
console.log('constructor state', this.state);
}
componentDidMount(): void {
// if user not found, and login state updated, redirect to login page
this.redirectIfUserNotFound();
}
private redirectIfUserNotFound() {
if (this.state.isLoginActive === false) {
window.location.assign('/login');
}
}
private async onLoadAccountRetrieve() {
const res: AxiosResponse = await axios()
.then((res) => {
// original version - attempt 1
this.setState({ account: res.data, isLoginActive: true });
})
// updated version - attempt 2 - code was moved here
return this.setState({ account: res.data, isLoginActive: true });
}
What I'm seeing is the onLoadAccountRetrieve method being run 4 times that fail, and another 12 times that return the account data in a console.log but the state variables don't get updated.
Questions:
How do I get react to not run the code 16 times?
How do I get my data to run before render()?
If I can get these two to be fixed, I'm expecting that the setState() (which isn't updating anything now) would self resolve itself.
Firefox Dev Browser Console:

Related

Value from BehaviorSubject is not rendered in the subscribed component

I'm passing a value from a service to a component using BehaviorSubject -
In serviceFile.service.ts:
taskComplete = new BehaviorSubject<{ complete: Boolean; error: any }>(null);
...
this.taskComplete.next({ complete: false, error: error });
...
In componentFile.component.ts:
ngOnInit() {
this.taskCompleteSub = this.myService.taskComplete.subscribe(
(data) => {
this.error = data.error
? data.error.error.message
: null;
console.log(this.error);
}
);
}
The problem is that the value of property this.error is changed and printed in console.log(), but this change is not reflected in the component template. In other words, angular does not check this change and re-render.
You are initializing your taskComplete BehaviorSubject with null, so that's the first value emitted. However, in your component you are trying to access data.error when data is null for the first value emitted. The following should work:
this.error = data && data.error && data.error.error
? data.error.error.message
: null;
I created this working example: https://stackblitz.com/edit/angular-fh6cfg?file=src%2Fapp%2Fapp.component.ts
If this.myService.taskComplete is an asynchronous action you'll need to manually trigger change detection.
constructor(private cdr: ChangeDetectorRef) { }
...
ngOnInit() {
this.taskCompleteSub = this.myService.taskComplete.subscribe(
(data) => {
this.error = ...;
this.cdr.markForCheck();
}
);
}
I'd suggest two changes.
If the default value of the BehaviourSubject is null and if you're forced to check if the value is null in each of it's subscription, you're better off using a ReplaySubject with buffer 1 instead. It'll buffer/hold the last value similar to BehaviorSubject but doesn't require a default value.
If the object's underlying reference hasn't changed, the Angular change detection may detect any changes to re-render the template. In that case try to make a hard-copy using JSON.parse(JSON.stringify()).
Service
taskComplete = new ReplaySubject<{ complete: Boolean; error: any }>(1);
Component
ngOnInit() {
this.taskCompleteSub = this.myService.taskComplete.subscribe(
(data) => {
// `null` check not required here now
this.error = JSON.parse(JSON.stringify(data.error.error.message));
console.log(this.error);
}
);
}

The entity passed to the `selectId` implementation returned undefined

i want to set token and refresh token in ngrx and save and use that for every request and when the page was reload not delete data in redux .
i implementation this state for this :
i dispatch data :
this.getUserInformation().toPromise().then(responset => {
if (response.success === true) {
this.store.dispatch(new SetUserInformation({
displayName: responset['result']['displayName'],
userInfo: responset.result.claims,
RefreshTokenStatus:false,
accessToken:response['result']['access_token'],
refreshToken:response['result']['refresh_token']
}))
}
});
in module i definde store :
StoreModule.forFeature('Information', reducer),
EffectsModule.forFeature([])
this is my reducer :
const initialState = adapter.getInitialState({
accessToken: null,
refreshToken: null,
RefreshTokenStatus: false,
userInfo: null,
displayName: null
})
export function reducer(state = initialState, action: TokenAction): TokenState {
switch (action.type) {
case TokenActionTypes.UserInformation:
return adapter.addOne(action.payload, state)
default:
return state;
}
}
and this is my model :
export interface TokenState {
accessToken: string;
refreshToken: string;
RefreshTokenStatus: boolean;
userInfo: string[];
displayName: string;
}
this is my Selector:
export interface State extends fromState.State, EntityState<TokenState> {
UserInfo: TokenState;
}
export const adapter: EntityAdapter<TokenState> = createEntityAdapter<TokenState>();
const getTokenFetureState = createFeatureSelector<TokenState>('Information');
export const getAccessToken=createSelector(
getTokenFetureState,
state => state.accessToken
)
this is action :
export class SetUserInformation implements Action {
readonly type = TokenActionTypes.UserInformation;
constructor(public payload: TokenState) {
}
}
export type TokenAction = SetUserInformation
now i have tow problem :
this.store.pipe(select(fromTokenSelect.getAccessToken)).subscribe(data=>{
console.log(data)
})
A: when i want to get token for use that it return null and show me this error :
#ngrx/entity: The entity passed to the selectId implementation returned undefined. You should probably provide your own selectId implementation. The entity that was passed: Object The selectId implementation: (instance) => instance.id
B: when reload the page it delete data from ngrx.
how can i solve this problem ?
You are using ngrx/entity for non-entity data. Entity is designed to work with arrays of data, where each element has some kind of unique identifier. That is why it is throwing the error: you're feeding it non-entity-compatible data. You need to write a simple reducer which simply sets the state, not one which tries to add data to a non-existent entity.
As for page reload, NgRx will always reset the data, that is the expected behavior. You need to implement localStorage saving if you want to persist data over time.

The most robust way to setState in React JS

I have a firebase realtime database, a JSON based no sql database, I have 5 fields in that database, storing these:
field 1: STRING
field 2: STRING
field 3: STRING
field 4: STRING
field 5: ARRAY OF OBJECTS
so that database with real example looks something like this:
name: STRING
age: NUMBER
email: STRING
phoneNumber: STRING
skills: OBJECT[]
now I am downloading all these info from the firebase as a single variable(var) called
snapshot.val(). and then updating all the states using setState at once like this:
this.setState({
state_name: snapshot.val().name,
state_age: snapshot.val().age,
state_email: snapshot.val().email,
state_phoneNumber: snapshot.val().phoneNumber,
state_skills: snapshot.val().skills
});
now this statement works fine if all the values are there in the database and it also works fine if the strings aren't there in the database but it runs into an error if the skills field is undefined in the database.
So setState doesn't throw an error when strings are undefined but it does throw error when arrays are undefined. it happened even with simple string array. Now if one field doesn't exist and an error is thrown, the entire setState fails, so in summary, if multiples states are being updated at once and one of the state upadate fails with an exception or error, all the updates fail and I have been updating such delicate data with separate setState statements with only one state being updated inside its own try catch block. So to update multiple states that have the potential to crash, I am using multiple try catch blocks. something like this:
try {
this.setState({
ownerNameValue: snapshot.val().owner_name,
});
} catch {}
try {
this.setState({
phoneNumberValue: snapshot.val().phone_number,
});
} catch {}
try {
this.setState({
specialityValue: snapshot.val().specialty,
});
} catch {}
try {
if (snapshot.val().services != undefined) {
this.setState({
selectedOptionSerivce: snapshot.val().services,
});
}
} catch {}
try {
if (snapshot.val().home_service != undefined) {
this.setState({
selectedOption: snapshot.val().home_service,
});
}
} catch {}
Now is there a more robust and efficient way to achieve this?
Some way to update all the states at once and not crash if one of the state has its value as undefined or simply fails. So that way, the states that run into no problems, get updated, but the states that run into exceptions and errors, simply get discarded without showing any error.
so it will be something like this:
field 1: STRING ->> state updated
field 2: STRING ->> state updated
field 3: STRING ->> state updated
field 4: STRING ->> state updated
field 5: undefined ->> state update FAILED (an array of objects was excepted)
NOTICE: this question is not limited to firebase, its about setState handling exceptions and errors of all kind.
You can assign to skills an empty array when it is undefined like this:
this.setState({
state_name: snapshot.val().name,
state_age: snapshot.val().age,
state_email: snapshot.val().email,
state_phoneNumber: snapshot.val().phoneNumber,
state_skills: snapshot.val().skills || [],
});
There is absolutely no sense in doing something like:
try {
if (snapshot.val().home_service != undefined) {
this.setState({
selectedOption: snapshot.val().home_service
});
}
} catch{ }
Calling setState won't crash if you set undefined as a value. The only case you may face an error is when you accidentally forgot to check this value inside of a lifecycle methods or render:
render() {
return this.state.skills.map(...)
}
Here, if skills is undefined you'll get an error. For efficiency sake you may do something like:
class Some {
constructor() {
this.state = {...}
}
get skills() {
return this.state.skills || []
}
render() {
return this.skills.map(...)
}
}
First, you don't need to wrap this.setState inside of a try/catch. setState will not throw an error if you try to set some property of your state to undefined - this is definitely allowed.
class App extends React.Component {
constructor(props) {
super(props);
this.state = { foo: "bar" };
}
componentDidMount() {
setTimeout(() => {
this.setState({
foo: undefined
});
}, 4000);
}
render() {
return this.state.foo !== undefined ? (
<h1>this.state.foo is not undefined</h1>
) : (
<h1>this.state.foo IS undefined</h1>
);
}
}
ReactDOM.render(
<App />,
document.getElementById("app")
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="app"></div>
So, when you say that it "fails" because you set a piece of state to undefined, you're most likely not handling this case elsewhere in your code. This is common when you're expecting something to be an array and you're trying to use the map method, but it turns out to be undefined.
render() {
// this.state.skills is `undefined
// and you're trying to use the `map` method
// which will fail
return (
<div>
{this.state.skills.map(...)}
</div>
)
}
What you should do is to specify fallbacks for items that come back as undefined:
class App extends React.Component {
updateState = () => {
firebase()
.then(snapshot => {
this.setState({
state_name: snapshot.val().name || '',
state_age: snapshot.val().age || 0,
state_email: snapshot.val().email || '',
state_phoneNumber: snapshot.val().phoneNumber || '',
state_skills: snapshot.val().skills || []
})
})
.catch(/* handle errors appropriately */)
}
}

React - getDerivedStateFromProps and axios

I would like to set state variable books as result of API. I cannot do it in componentDidMount, because I don't have token at the beginning and need it to get result from API. Nothing happens with state books when I run following code. If I put state.books=res.data before return I got a result, but after manually refresh page.
constructor(props) {
super(props);
this.state = {
books: [],
};
}
and
static getDerivedStateFromProps(nextProps, state) {
if (nextProps.token){
axios.defaults.headers = {
"Content-Type": "application/json",
Authorization: "Token " + nextProps.token
}
axios.get('http://127.0.0.1:8000/api/book/own/')
.then(res => {
return {
books: res.data,
}
})
}
data from the API looks like:
{
id: 66,
isbn: "9780545010221",
title: "Title",
author: "Author,Author",
}
In the render method I call component with this.static.books data.
Could you please advise me?
This is a very common pitfall: you are returning something inside the promise handler (then), thinking that that would return from the function that created the promise (getDerivedStateFromProps). That's not the case.
I'm afraid you can't use getDerivedStateFromProps for asynchronous code like this. However, you don't have to, given that react is, uhm, reactive.
componentDidUpdate() {
if (this.props.token) {
axios.get('http://127.0.0.1:8000/api/book/own/')
.then(res => this.setState({books: res.data})) ;
}
}

Handling multiple axios get requests inside componentDidMount

I have a React component like this:
class Example extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
address: '',
phone: ''
}
}
componentDidMount() {
//APIcall1 to get name and set the state
//i.e., axios.get().then(this.setState())
//APIcall2 to get address and set the state
//APIcall3 to get phone and set the state
}
}`
As you can see I am making three API get requests to get the details and setting the state three times after getting the data. Due to this, I am getting this error:
Warning: Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
By the way, I am not causing a state change in the render method. Anyway to solve this issue?
As axios.get returns a promise, you can link them together before calling setState. For example using Promise.all:
componentDidMount() {
Promise.all([
APIcall1, // i.e. axios.get(something)
APIcall2,
APIcall3
]).then(([result1, result2, result3]) => {
// call setState here
})
}
Just beware that if any of the api calls fail, Promise.all will catch, and setState won't be called.
In axios you have the method axios.all:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// Both requests are now complete
}));
Or you can use the standard Promise.all:
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
Promise.all([getUserAccount(), getUserPermissions()])
.then(data => {
// Both requests are now complete
});

Categories