setState does not trigger render after item deleted from object - javascript

In my app you can set an age restriction on a link. If there is an age restriction on the link will it have an item named ageRestriction in the link state which contains an int which is the age. If it does not have an age restriction there will be none.
When i chance rather or not the link has an age restriction the follow code runs:
useEffect(async () => {
if(ageRestriction === false) {
const newLink = link;
delete newLink.ageRestriction
setLink(newLink)
}
else if(ageRestriction === true) setLink({...link, ageRestriction: storedAge})
}, [ageRestriction])
which should then trigger the following code:
useEffect(() => {
const validate = validateLink(link)
if(validate) {
setError(validate)
}
setError({})
dispatch({type: 'update', link: link, index: index})
}, [link])
If I add an age restriction in the first piece of code the useEffect in the second piece of code trigger. If I delete the ageRestriction item from the object and set the state the useEffect in the second piece of code does not trigger. What can I do to fix this thanks! in advance.

The dependency of your second useEffect should be link.ageRestriction because the comparison between objects is done by reference, thus removing the property from the link object doesn't trigger the effect:
useEffect(() => {
const validate = validateLink(link)
if(validate) {
setError(validate)
}
setError({})
dispatch({type: 'update', link: link, index: index})
}, [link.ageRestriction])
Alternatively, you can create a new object when removing the property in the first useEffect as follow:
useEffect(async () => {
if(ageRestriction === false) {
const newLink = link;
delete newLink.ageRestriction
setLink({ ...newLink })
}
else if(ageRestriction === true) setLink({...link, ageRestriction: storedAge})
}, [ageRestriction])

Related

Updating an object property within an array of objects in React

I am on the newer side of React and trying to change the state of an object in an array. Currently, I am pulling the object out of the array, changing the property in that object, then adding the new object to the state again. Problem being that it sends the object to the back of the list and reorders my checkbox inputs.
const handleChange = (e) => {
if (e.target.type === "checkbox") {
// Get the role from the current state
const roleToChange = input.roles.find(
(role) => Number(role.id) === Number(e.target.id)
);
// Change checked state to opposite of current state
const changedRole = { ...roleToChange, checked: !roleToChange.checked };
// Get every role except the one that was changed
const newRoles = input.roles.filter(
(role) => Number(role.id) !== Number(e.target.id)
);
// Update the role in the state
setInput((prevState) => {
return { ...prevState, roles: [...newRoles, changedRole] };
});
}
Can I update the object in the array in-place so this doesn't happen?
Don't .filter - .map instead, and return the changed object in case the ID matches, so the new object gets put at the same place in the new array as it was originally.
const handleChange = (e) => {
if (e.target.type !== "checkbox") {
return;
};
const newRoles = input.roles.map((role) =>
Number(role.id) !== Number(e.target.id)
? role
: { ...role, checked: !role.checked }
);
setInput((prevState) => {
return {
...prevState,
roles: newRoles
};
});
}
Unless the state is updated synchronously before this, which sounds a bit unlikely (but not impossible), you can also probably use setInput({ ...input, roles: newRules }) instead of the callback.

How to get the value of useRef current in React Native

I am developing a React Native application and am facing the following error:
I have defined a useRef which stores the doc ID from a firebase collection. But when I call that variable after it has been defined, the .current value returns a blank string.
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
console.log(bidId.current)
}
})
})
The above code returns the expected value. However, when I call the variable outside this db.collection loop, I get the following value:
But calling the bidId.current returns a blank string.
Please can someone help me with this. Thanks!
Actually this is what happens:
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
bidId.current = doc.id
// This line gets executed after some time!
console.log(bidId.current)
}
})
})
// This gets executed first! (The value has not been stored yet!)
console.log(bidId.current);
Using the "useState" hook instead of "useRef" will solve the issue. Consider the following code:
const [BidId, setBidId] = useState<string | null>(null);
// This will store the value...
useEffect(() => {
db.collection('users').onSnapshot((snapshot) => {
snapshot.docs.map((doc) => {
if (doc.data().email === auth.currentUser?.email) {
setBidId(doc.id);
}
})
})
}, []);
// Here you can access the value
useEffect(() => {
if(BidId !== null)
console.log(BidId);
}, [BidId]);
// You can also return the component like the following:
return (<View>The Bid ID is: {BidId !== null ? BidId : "Loading..."}</View>);
Your useEffect basically says that whenever pageRef changes, call this function. If done outside, it will call do your tasks on every render instead of doing the whenever pageRef values is changed. Also, in initial renders, it may give undefined values.
You can only return a function in useEffect which basically says that before running the same next time, run this function before.
Try (currentUser without the '?' query character):
if (doc.data().email === auth.currentUser.email) {
bidId.current = doc.id
console.log(bidId.current)
}

Why is my globalState state object being updated before I update it myself?

I have a multitabbed view that I am controlling the data with through a global state, being passed through useContext (along with the setState updater function).
The structure is similar to
globalState: {
company: {
list: [
[{name: ..., ...}, {field1: ..., ... }],
[{name: ..., ...}, {field1: ..., ... }],
...
]
},
...
}
I have a table in this first tab, where each row that displays the details in the first object of each inner list array (globalState.company.list[X][0]), and has a few checkboxes to toggle fields in the second object in each inner list array (globalState.company.list[X][1]).
The issue I am having is that when I check a checkbox for a specific field, all companies have that field set to that value before I call setGlobalState(...) in that onChange call from the checkbox itself.
Here is all the related code for the flow of creating the checkbox and the handler:
<td><Checkbox
disabled={tpr.disabled} // true or false
checked={tpr.checked} // true or false
onChange={checkboxOnChange} // function handler
targetId={company.id} // number
field={"field1"} />
</td>
Checkbox definition
const Checkbox = React.memo(({ disabled, checked, onChange, targetId, field }) => {
return (
<input
type="checkbox"
style={ /* omitted */ }
defaultChecked={checked}
disabled={disabled}
onChange={(e) => onChange(e, targetId, field)}
/>
);
});
onChange Handler callback
const checkboxOnChange = (e, id, field) => {
const checked = e.target.checked;
console.log("GLOBAL STATE PRE", globalState.companies.list);
let foundCompany = globalState.companies.list.find(company => company[0].id === id);
foundCompany[1][field].checked = checked;
console.log("foundCompany", foundCompany);
console.log("GLOBAL STATE POST", globalState.companies.list);
setGlobalState(prevState => ({
...prevState,
companies: {
...prevState.companies,
list: prevState.companies.list.map(company => {
console.log("company PRE ANYTHING", company);
if (company[0].id === foundCompany[0].id) {
console.log("Inside here");
return foundCompany;
}
console.log("company", company);
return company;
})
}
}));
};
I see from the GLOBAL STATE PRE log that if I were to check a box for field1, then all companies would have field1 checked well before I modify anything else. I can confirm that before the box is checked, the globalState is as I expect it to be with all of the data and fields correctly set on load.
In the picture below, I checked the box for TPR in the second company array, and before anything else happens, the second and third companies already have the TPR set to true.
Any help would be appreciated, and I will share any more details I am able to share. Thank you.
Just don't mutate the state object directly:
const checkboxOnChange = (e, id, field) => {
const checked = e.target.checked;
setGlobalState(prevState => ({
...prevState,
companies: {
...prevState.companies,
list: prevState.companies.list.map(company => {
if (company[0].id === id) {
return {
...company,
checked
};
}
return {
...company
};
})
}
}));
};
The globalState object is being updated before you call setGlobalState because you are mutating the current state (e.g. foundCompany[1][field].checked = checked;)
One way of getting around this issue is to make a copy of the state object so that it does not refer to the current state. e.g.
var cloneDeep = require('lodash.clonedeep');
...
let clonedGlobalState = cloneDeep(globalState);
let foundCompany = clonedGlobalState.companies.list.find(company => company[0].id === id);
foundCompany[1][field].checked = checked;
I recommend using a deep clone function like Lodash's cloneDeep as using the spread operator to create a copy in your instance will create a shallow copy of the objects within your list array.
Once you have cloned the state you can safely update it to your new desired state (i.e. without worry of mutating the existing globalState object) and then refer to it when calling setGlobalState.

Push and pop on react state

i have this react state in mi component
this.state =
{
Preferences: []
}
and i want to push only if the element not exists because i dont want the same repeated element, and i want to pop the element if is already exists this is the function what i use
SelectPreference = Id =>
{
if(!this.state.Preferences.includes(Id))
{
this.setState(state =>
{
const Preferences = state.Preferences.concat(Id);
return {
Preferences,
};
});
}
else
{
this.setState(state =>
{
const Preferences = state.Preferences.filter(Item => Item !== Id);
return {
Preferences,
};
});
}
console.log(this.state.Preferences);
}
the problem is when i push a object it says the array is empty and when i pop the element it says i have a element. i dont kwon how to do it
You can do as the following if you want your preferences to be uniq.
Based on your code, I understand that id is a string. Tell me if it's not the case.
For more info about Set and uniq value in array
if(!this.state.Preferences.includes(Id)) {
this.setState(state => ({
// Using a set will make sure your id is uniq
Preferences: [...new Set([...state.Preferences, id])]
}))
}
else {
this.setState(state => ({
// You're code was good, I only change syntax
Preferences: state.Preferences.filter(Item => Item !== Id)
}))
}
setState actions are asynchronous and are batched for performance gains. It will be pretty hard to immediately check the updated this.state.Preferences in the same function block.
Still if you need to check the updated value of the Preferences, you can provide a callback which will be executed immediately after change in state.
this.setState({ Preferences }, () => console.log(this.state))
Also, there is simpler version of your logic.
if(this.state.Preferences.includes(Id)) {
this.setState({
Preferences: this.state.Preferences.filter(id => id !== Id);
});
} else {
this.setState({
Preferences: this.state.Preferences.concat(Id);
});
}

useEffect with callback not updating state

I'm trying to re-render a component to hide or show a 'Use current location' button when the user blocks or allows their location to be known (by clicking on the info icon to the left of the browser address in Chrome). I'm totally confused by why the first example below works, (ie. the button toggled appropriately as soon as the permission is changed) but the second doesn't (it requires a refresh).
I was attemping to remove the duplicate permission.state !== 'denied' by simply defining the constant hasPermission.
Also, I don't clean up the onchange listener. How do I do this? Can I just assign to null or delete the property?
Works:
useEffect(() => {
navigator.permissions.query({ name: 'geolocation' }).then(permission => {
setHasPermission(permission.state !== 'denied')
permission.onchange = () =>
setHasPermission(permission.state !== 'denied')
})
}, [])
Doesn't work:
useEffect(() => {
navigator.permissions.query({ name: 'geolocation' }).then(permission => {
const hasPermission = permission.state !== 'denied';
setHasPermission(hasPermission)
permission.onchange = () =>
setHasPermission(hasPermission)
})
}, [])
Basically, permission.onchange is defined only once.
So, when you define, in your second example, permission.onchange = () => setHasPermission(hasPermission) you create an event listener that will always call setHasPermission with the same value: the result of permission.state !== 'denied' saved above.
In your first example, it works because permission.state !== 'denied' is evaluated in the event callback.
Is it clear enough ?
For cleanup, useEffect can return a function that will be called when component unmounts. Please check https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1

Categories