useEffect() doesn't trigger on property object change (Deep object) - javascript

I have a component which takes a deep object as argument. I want to be able to dynamically alter my component based on the property that I pass - But for some reason my useEffect() loop doesn't run the 2nd time that I pass an object, which naturally is due to the fact that it is a deep object which useEffect() doesn't "recognize"
My code is as follows:
<MyComponent groups={sourceArray}/>
function MyComponent({ groups }) {
useEffect(() => {
//Do something
}, [groups])
For clarification, my data is an array of objects.
I did try several things to get this to work - but for some reason, I just cant get my effect loop to trigger:
First alternative solution which doesn't work (useRef)
useEffect(() => {
if (prevGroups && !isEqual(prevGroups, groups)) {
// do something
}
}, [groups])
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const prevGroups = usePrevious(groups);
2nd solution which also fails (External library)
I tried using the following library, - Also without any luck.
Conclusively, I'm unsure what's going on here and how I would get my useEffect loop to run on update.
The easy solution would be to remove the dependency array - But that just makes the useEffect loop run infinitely
Update
object has the following structure:
[{"name": "123", "id": "1", "children": [{...}]}]
Also, I'm passing the value as a useState(), i.e.:
const funcToChangeObj = () => {
//logic to change sourceArray
setSourceArray(changedArray)
}

setSourceArray(changedArray) -> setSourceArray(changedArray.slice())

Related

state update from a callback

The following member function populates asynchronously a folder_structure object with fake data:
fake(folders_: number, progress_callback_: (progress_: number) => void = (progress_: number) => null): Promise<boolean>
{
return new Promise((resolve, reject) => {
for (let i_ = 0; i_ < folders_; i_++) {
progress_callback_(i_ / folders_ * 100.);
this.add(this.id(), faker.address.city() + i_, random_choice(this.folder_structure_id()));
}
progress_callback_(folders_ / folders_ * 100.);
resolve(true);
})
}
It uses a callback to update the progress within the for loop which is then used to update the state (a progress bar) from within a useEffect() function with an empty dependency array.
let [progress_state_, set_progress_state_] = useState<number>(0);
let [fake_done_, set_fake_done_] = useState<boolean>(false);
useEffect(() =>
{
if (fake_)
folder_structure_.fake(fake_, (progress_) => {
set_progress_state_(progress_)
}).then(value => set_fake_done_(value));
}, [])
if (!fake_ || fake_done_) etc etc
However, the state is not updated (logging the progress in the console seems to work fine). Any ideas as to whether it's possible to update a state from within useEffect?
The reason your useEffect hook isn't working is that it's not called upon progress_state_ state change.
Instead of
useEffect(() =>
{
...
}, [])
Try this instead
useEffect(() =>
{
...
}, [progress_])
Adding progress_ to the dependency array means useEffect will be called every single time progress_ changes. If you leave it as an empty dependency array, then useEffect is only ever called in the very beginning on when the code is mounted to the DOM.
Here's a good explanation on dependency arrays: https://devtrium.com/posts/dependency-arrays
Addressing your final question: Yes, it is possible to update state from within useEffect.
To understand the root of your main issue, I would be curious to see how you are doing your logging. Are you logging from within fake() or from your render() function?

Why is react useState not updating the values? [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Best way to request unknown number of API pages in useEffect hook
(4 answers)
Closed 2 years ago.
I am using an API to fetch data, but in order to fetch all the data I am required to loop through the links
const [characterData, setCharacterData] = useState([]);
const [link, setLink] = useState("https://swapi.dev/api/people/");
useEffect(() => {
getData();
}, [])
async function getData() {
while (link) {
const fetchedData = await fetch(link);
const jsonData = await fetchedData.json();
setCharacterData([...characterData, jsonData.results]);
setLink(jsonData.next);
console.log(link)
}
}
This is how one of the jsonData from above would look like:
{
"next": "http://swapi.dev/api/people/?page=2", "previous": null, "results": [list of the wanted data] }
The last object will have "next": null, so the while loop should end at some point, but for some reason setLink() never updates the link and causes it to become an infinite loop. Link always remains as "https://swapi.dev/api/people/".
Another problem is that the page isn't displaying anything as characterData gets updated, so I am assuming that characterData isn't updating either, but I am not sure.
characterData.map((character, index) => {
return (
<div className="character-item" key={index}>
<h4>{character.name}</h4>
<p>{character.birth_year}</p>
</div>
);
})
Note: Each character is an object
Thank you!
The link is declared with const - it'll never change in a given render. So the
while (link) {
will run forever.
Unless you're using the link elsewhere, I'd remove it from state entirely and use a local variable inside getData instead. Make sure to use the callback form of setCharacterData too so that you don't lose the prior value in state.
async function getData() {
let link = 'https://swapi.dev/api/people/';
while (link) {
const fetchedData = await fetch(link);
const jsonData = await fetchedData.json();
setCharacterData(characterData => [...characterData, jsonData.results]);
link = jsonData.next;
}
}
It would also be a great idea to catch possible errors:
useEffect(() => {
getData()
.catch(handleErrors);
}, [])

Appending multi dimensional array in react state

I'm trying to append array which is react state:
const [ products, setProducts ] = useState([])
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
const copy = JSON.parse(JSON.stringify(products))
copy[category.id] = data
setProducts(copy)
})
})
},[])
service.getCategory() fetches data over HTTP returning array. products is nested array, or at least it's suppose to be. config.category is defined as:
categories: [
{
name: 'product1',
id: 0
},
{
name: 'product2',
id: 1
},
{
name: 'product3',
id: 2
}]
}
Eventually products should be appended 3 times and it should contain 3 arrays containing products from these categories. Instead products array ends up including only data from last HTTP fetch, meaning the final array looks something like this
products = [null, null, [{},{},{},..{}]].
I hope someone knows what's going on? Been tinkering with this for a while now.
The problem is that your fulfillment handlers close over a stale copy of products (the empty array that's part of the initial state). In a useEffect (or useCallback or useMemo, etc.) hook, you can't use any state items that aren't part of the dependency array that you provide to the hook. In your case, you just want to get the data on mount, so an empty dependency array is correct. That means you can't use any state items in the callback.
What you can do instead is use the callback form of the state setter:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => { // Use the callback form
const copy = products.slice(); // Shallow copy of array
copy[category.id] = data; // Set this data
return copy; // Return the shallow copy
});
});
});
}, []);
Or more concisely (but harder to debug!) without the explanatory comments:
const [ products, setProducts ] = useState([]);
useEffect(() => {
config.categories.forEach(category => {
service.getCategory(category.name).then(data => {
setProducts(products => Object.assign([], products, {[category.id]: data}));
});
});
}, []);
Those both use the same logic as your original code, but update the array correctly. (They also only make a shallow copy of the array. There's no need for a deep copy, we're not modifying any of the objects, just the array itself.)
But, that does a state update each time getCategory completes — so, three times in your example of three categories. If it happens that the request for id 2 completes before the request for id 1 or 0, your array will look like this after the first state update:
[undefined, undefined, {/*data for id = 2*/}]
You'll need to be sure that you handle those undefined entries when rendering your component.

Array in data method gets updated in one method but still empty in another method

In my app I am trying to update an array. First I get data from the database and add it to the array and in another method I want to use that array. But the array does not get updated.
If I use my array exerciseList in the DOM it has the data but in the getExercises funciton the length of the array is still 0. It like I run the method before the data is added to the array or something like that.
Any Idea why this is not working?
data: () => ({
exerciseList: []
});
created() {
this.getDataBaseCollection("Exercise", this.exerciseList); // array gets information here
}
mounted() {
this.getExercises();
},
methods: {
getDataBaseCollection: function (CollectionName, list) {
db.collection(CollectionName).onSnapshot(snapshot => {
snapshot.forEach(doc => {
list.push(doc.data());
});
});
},
getExercises: function () {
console.log(this.exerciseList.length); // length is 0 ?? array seems empty
}
},
I think a key missing part may be updating the component's exerciseList variable , not the list argument. They are not the same variable. Objects are passed by reference but arrays are passed to functions by value only which makes list it's own variable independent from excerciseList. This is rough code that shows some ways to make sure exerciseList is updated and how to know when the values are all in the array.
// include exerciseListLoaded to flag when all data is ready
data: () => ({
exerciseList: [],
exerciseListLoaded: false
});
created() {
this.getDataBaseCollection("Exercise"); // array gets information here
}
mounted() {
// based on timing of the `onSnapshot` callback related to `mounted` being called, this may likely still result in 0
console.log("Mounted");
this.getExercises();
},
watch: {
// watch for all data being ready
exerciseListLoaded () {
console.log("All Loaded");
this.getExercises();
}
},
methods: {
// be sure to update the exerciseList on the component
getDataBaseCollection: function (CollectionName) {
// being careful about `this` since within `onSnapshot` I suspect it will change within that function
const componentScope = this;
db.collection(CollectionName).onSnapshot(snapshot => {
snapshot.forEach(doc => {
componentScope.exerciseList.push(doc.data());
// could also still update `list` here as well if needed
});
// setting this allows the component to do something when data is all loaded via the `watch` config
componentScope.exerciseListLoaded = true;
});
},
getExercises: function () {
console.log(this.exerciseList.length); // length is 0 ?? array seems empty
}
},
when you use this inside a function it refers to the function not the vue instance so you may use that may work with you:
getExercises() {
console.log(this.exerciseList.length);
}

Use spread operator to copy array state and add all objects that match

I am trying to sort all objects that match the regex into an array.
This does not seem to work with the spread operator and useState, is there any way I can do that?
The result I am getting now is the samples thing only gives me the last object that matches it and nothing else.
The desired effect I want is all the samples that match get pushed into the samples state.
const [accessories, setAccessories] = useState([]);
const [paints, setPaints] = useState([]);
const [samples, setSamples] = useState([]);
// Load order into state
useEffect(() => {
loadUser();
getOrderById(match.params.orderId);
}, []);
// Load order into state
useEffect(() => {
if (!loading) {
console.log(order.line_items);
for (let i = 0; i < order.line_items.length; i++) {
if (order.line_items[i].sku.match(/^(TAC|T.BU.AC)/)) {
console.log('SKU: ', order.line_items[i].sku);
//#ts-ignore
setAccessories([...accessories, order.line_items[i]]);
console.log(accessories);
}
if (order.line_items[i].sku.startsWith('TBA') || order.line_items[i].sku.match(/^TCR(?!0000)/)
|| order.line_items[i].sku.match(/^TCR0000/)) {
//#ts-ignore
setPaints([...paints, order.line_items[i]]);
}
if (order.line_items[i].sku.match(/^TCR\d+P?\d+SAMP/)) {
console.log(samples);
console.log(order.line_items[i]);
//#ts-ignore
setSamples([...samples, ...[order.line_items[i]]]);
}
}
}
}, [loading]);
Well there are few mistakes you're doing here.
Mistake 1:
Calling the same setStates way too many times inside a single useEffect block using a for loop, this might greatly affect React's performance. Again, this is clearly a violation of Rules of Hooks, Only Call Hooks at the Top Level
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions.
Mistake 2:
Though this is not as serious as the previous ones, it's still a mistake. Not using better solutions, Use inbuilt JavaScript methods like filter instead of writing your own for loop
useEffect(() => {
let _accessories;
let _paints;
let _samples;
if (!loading) {
_accessories = order.line_items.filter(({ sku }) => sku.match(/^(TAC|T.BU.AC)/))
_paints = order.line_items.filter(({ sku }) => sku.startsWith('TBA') || sku.match(/^TCR(?!0000)|^TCR0000/))
_samples = order.line_items.filter(({ sku }) => sku.match(/^TCR\d+P?\d+SAMP/))
// Never use setState inside a for loop
// of useEffects
// Also avoid calling same setState multiple times
// use callback setState if you want to access
// previous state, but it ain't a compulsory like
// for class components
setAccessories(s => [ ...s, ..._accessories ])
setPaints(s => [ ...s, ..._paints ])
setSamples(s => [ ...s, ..._samples ])
}
// return in useEffect has different role
// than normal functions
}, [loading])
Spread the results of calling .filter into the calls:
useEffect(() => {
if (loading) {
return;
}
const items = order.line_items;
setAccessories([
...accessories,
items.filter(({ sku }) => sku.match(/^(TAC|T.BU.AC)/))
]);
setPaints([
...paints,
items.filter(({ sku }) => sku.startsWith('TBA') || sku.match(/^TCR(?!0000)|^TCR0000/))
]);
setSamples([
...samples,
items.filter(item => item.sku.match(/^TCR\d+P?\d+SAMP/))
]);
}, [loading]);

Categories