The most robust way to setState in React JS - javascript

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 */)
}
}

Related

React Class Runs Code Excessively and Does Not Update State

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:

Vue FireStore Reactive Key Assignment - Object is possibly 'undefined'

I am attempting to assign FireStore data(), forwarded by props, to a reactive() proxy object, but am receiving the following error:
Object is possibly 'undefined'.
(property) fireStoreData: Record<string, any> | undefined
I wish to use a forEach loop instead of direct assignments i.e. (pageForm.name = props.fireStoreData?.name) to minimize code.
props: {
fireStoreData: Object,
}
...
setup(props){
...
const pageForm: { [key: string]: any } = reactive({
name: '',
address: '',
...
})
onMounted(() => {
Object.keys(pageForm).forEach(key => {
if(key in props.fireStoreData) // <-- error found here
pageForm[key] = props.fireStoreData[key] // <-- and here
})
})
})
...
}
The issie is that the fireStoreData prop is not required yet in your code you assume it is.
try:
props: {
fireStoreData: {
required: true,
type: Object
}
}
If you dont want the prop to be required, its always possible to check for it being defined and add a watcher to look for changes.
Also, dont forget props can change in value.
// on mobile, will format later on pc

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);
}
);
}

Error when passing setState in a React class component to an external function?

Assume data has already been cached in sessionStorage. I have hydrateStateWithSessionStorage in an external CacheService.js file. I import this file. When I try to pass this.setState to this function, I get this error:
Uncaught TypeError: Cannot read property 'updater' of undefined
How can I solve this? I could possibly use a the React hook useState and pass the setter function, but what if I want to use a class component instead of functional component? Or am I simply unable to pass setState because it implicitly uses the 'this' keyword in its implementation?
hydrateStateWithSessionStorage(state, setState) {
// for all items in state
for (let key in state) {
// if the key exists in localStorage
if (sessionStorage.hasOwnProperty(key)) {
// get the key's value from localStorage
let value = sessionStorage.getItem(key);
// console.log(value)
// parse the localStorage string and setState
try {
value = JSON.parse(value);
console.log('before')
setState({ [key]: value });
console.log('after')
} catch (e) {
// handle empty string
setState({ [key]: value });
}
}
}
}
//in the component consuming CacheService
//this.Cache = new CacheService(); //in the constructor
componentDidMount() {
this.Cache.hydrateStateWithLocalStorage(this.state, this.setState);
this.Auth.fetch('api/upcomingbill/').then((data) => {
this.setState({ list: data })
});
}
I would treat this function more of a check return sessionStorage object if nothing return undefined. Send this.state into the fn() then check response and return the response.
componentDidMount() {
const newState = hydrateState(this.state):
!!newState && this.setState(newState)
}
Just a brain dump..
How about this?
componentDidMount() {
this.Cache.hydrateStateWithLocalStorage(this);
this.Auth.fetch('api/upcomingbill/').then((data) => {
this.setState({ list: data })
});
}
And then using setState like so...
hydrateStateWithSessionStorage(ReactComp) {
for (let key in ReactComp.state) {
...
ReactComp.setState({ [key]: value });
....
}
}
FYI, this is not a React specific issue. The reason you are getting that error is, you are passing just a method that internally uses "this" (of that particular class) to the hydrateStateWithSessionStorage function. By the time, that method is called in the function, the scope has changed and "this" is not defined or not the same anymore.

Vuejs - Vuex behavior when committing a property

I'm noticing a weird behavior in Vuex when I try to mutate a property in the state obj.
Example:
Mutation: {
authUser: (state, payload) => {
state.email = payload.email
state.password = payload.password
...someOtherProps
}
actions: {
commit ('authUser', {
email: 'user#gmail.com'
})
}
What I noticed is that when I commit only one property(in this case "email"), the value of all other properties of the authUser will be undefined and only email value will be available.
Is that the way Vuex behaves in this case? If yes, how can I avoid the other props not getting a empty value?
Thanks
You're passing an object without a password property defined, so it's going to update the state object accordingly.
I'd just loop through the properties of the payload to update each related state object property. And as #82Tuskers pointed out, you'll need to use Vue.set if the property in the payload object doesn't yet exist on the state object (otherwise the property won't be reactive):
authUser: (state, payload) => {
for (prop in payload) {
if (state.hasOwnProperty(prop)) {
state[prop] = payload[prop];
} else {
Vue.set(state, prop, payload[prop]);
}
}
}
This way, only the properties being passed in the payload object will be updated on the state object.
It is not strange, it is expected behaviour. Just rewrite your mutation this (recommended) way:
authUser: (state, payload) => {
state = Object.assign({}, state, payload)
}
While the other answers seem to fix your issue, it might be worthwhile to put the user-related items into a user object inside the state. It is also best practice to establish your state properties up front so you can avoid having to use Vue.set(...):
state: {
user: {
email: '',
password: ''
}
}
...then you could easily avoid looping by using the spread operator: state.user = { ...state.user, ...payload } - this essentially says "take everything currently inside state.user and merge it with payload, overwriting what is in state.user with state.payload"

Categories