Vue deep copy object not changing value - javascript

I have a vue component that has a deep copy of an asset called cachedAsset. It has a value called toDelete to mark a file for soft deletion. I have the following code
markForDeletion(files) {
const thisComponent = this;
files.forEach((file) => {
thisComponent.cachedAsset.files.find(f => file.id === f.id).toDelete = true;
});
}
this works as intended it changes the .toDelete to true and the file is filterd out in a process further down the line.
The issue that I am having is with restoring the file back to the component with the following code
restoreFilesFromDeletion(items) {
items.forEach((item) => {
this.cachedAsset.files.find(f => item.id === f.id).toDelete = false;
});
}
With this code it should set the .toDelete back to false but it is not doing that and I get no errors or anything in the console.
Can anyone tell me why this is not updating the .toDelete back to false when executed .?
Edit 1
this is what I have now
restoreFilesFromDeletion(items) {
items.forEach((item) => {
let files = this.cachedAsset.files.find(f => item.id === f.id)
this.$set(files, 'toDelete', false)
});
}
it seems to be setting it but the true still does not change to false ..
am I still missing something ? any further advice that can be given...

It happens when data is deeply nested.
Try this Vue.delete.
and also see Reactivity in Depth.

After re-analyzing the results with some console tests, I found that I had an error elsewhere in my code, once I fixed that the delete and restore are working properly.
Thank you for your assistance, it has given me more knowledge and insight on reactivity and the $set method.

Related

Function A depends on previous function to update the state, but Function A still tries to render before the update

An Example I have linked below, that shows the problem I have.
My Problem
I have these two functions
const updatedDoc = checkForHeadings(stoneCtx, documentCtx); // returns object
documentCtx.setUserDocument(updatedDoc); // uses object to update state
and
convertUserDocument(stoneCtx, documentCtx.userDocument);
// uses State for further usage
The Problem I have is, that convertUserDocument runs with an empty state and throws an error and then runs again with the updated state. Since it already throws an error, I cannot continue to work with it.
I have tried several different approaches.
What I tried
In the beginning my code looked like this
checkForHeadings(stoneCtx, documentCtx);
// updated the state witch each new key:value inside the function
convertUserDocument(stoneCtx, documentCtx.userDocument);
// then this function was run; Error
Then I tried the version I had above, to first put everything into an object and update the state only once.
HavingconvertUserDocument be a callback inside of checkForHeadings, but that ran it that many times a matching key was found.
My current try was to put the both functions in seperate useEffects, one for inital render and one for the next render.
const isFirstRender = useRef(true);
let init = 0;
useEffect(() => {
init++;
console.log('Initial Render Number ' + init);
console.log(documentCtx);
const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
documentCtx.setUserDocument(updatedDoc);
console.log(updatedDoc);
console.log(documentCtx);
isFirstRender.current = false; // toggle flag after first render/mounting
console.log('Initial End Render Number ' + init);
}, []);
let update = 0;
useEffect(() => {
update++;
console.log('Update Render Number ' + update);
if (!isFirstRender.current) {
console.log('First Render has happened.');
convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
}
console.log('Update End Render Number ' + update);
}, [documentCtx]);
The interesting part with this was to see the difference between Codesandbox and my local development.
On Codesandbox Intial Render was called twice, but each time the counter didn't go up, it stayed at 1. On the other hand, on my local dev server, Initial Render was called only once.
On both version the second useEffect was called twice, but here also the counter didn't go up to 2, and stayed at 1.
Codesandbox:
Local Dev Server:
Short example of that:
let counter = 0;
useEffect(()=> {
counter++;
// this should only run once, but it does twice in the sandbox.
// but the counter is not going up to 2, but stays at 1
},[])
The same happens with the second useEffect, but on the second I get different results, but the counter stays at 1.
I was told this is due to a Stale Cloruse, but doesn't explain why the important bits don't work properly.
I got inspiration from here, to skip the initial render: https://stackoverflow.com/a/61612292/14103981
Code
Here is the Sandbox with the Problem displayed: https://codesandbox.io/s/nameless-wood-34ni5?file=/src/TextEditor.js
I have also create it on Stackblitz: https://react-v6wzqv.stackblitz.io
The error happens in this function:
function orderDocument(structure, doc, ordered) {
structure.forEach((el) => {
console.log(el.id);
console.log(doc);
// ordered.push(doc[el.id].headingHtml);
// if (el.children?.length) {
// orderDocument(el.children, doc, ordered);
// }
});
return ordered;
}
The commented out code throws the error. I am console.loggin el.id and doc, and in the console you can see, that doc is empty and thus cannot find doc[el.id].
Someone gave me this simple example to my problem, which sums it up pretty good.
useEffect(() => {
documentCtx.setUserDocument('ANYTHING');
console.log(documentCtx.userDocument);
});
The Console:
{}
ANYTHING
You can view it here: https://stackblitz.com/edit/react-f1hwky?file=src%2FTextEditor.js
I have come to a solution to my problem.
const isFirstRender = useRef(true);
useEffect(() => {
const updatedDoc = checkForHeadings(stoneCtx.stoneContext, documentCtx);
documentCtx.setUserDocument(updatedDoc);
}, []);
useEffect(() => {
if (!isFirstRender.current) {
convertUserDocument(stoneCtx.stoneContext, documentCtx.userDocument);
} else {
isFirstRender.current = false;
}
}, [documentCtx]);
Moving isFirstRender.current = false; to an else statement actually gives me the proper results I want.
Is this the best way of achieving it, or are there better ways?

Cannot display images in my JSX from my array.map in my react app even though .map loops though all index of array

The question: why doesn't react display my images in the JSX? How can I get it to display the images?
Edit: the issue I suspect is not with the way I get the data but rather the way I try to display/render the image, which I have labeled under main issue below.
I have a functional component that has a useEffect to GET a JSON object from firebase.
useEffect(()=>{
axios
.get("http://localhost:9998/api/v1/fault/" + lastURLSegment)
.then((response) => {
setDisplay(response.data)
})
},[])
Then I extract the imageURL value and query firebase to return me a url viewable on the web
e.g https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media
once I am able to get the URL for the image, I setState into an array called objUrl
edit: there is objOfUrl and objUrl, objOfUrl is a var [], objOfUrl is a state
useEffect(()=>{
// displayspecificCases.imgURL = "fault/asd.jpg"
let foo = displayspecificCases.imageurl // "fault/asdfsdf.jpg,fault/1234.jpg..."
if (foo !== undefined){
console.log(foo) //console logs 2 images in string saperated by comma
let bar = foo.split(','); // bar is now an array ['fault/asdfsdf.jpg','fault/1234.jpg']
let i = 0;
bar.map((item)=>{
console.log(item)
if(item !== ""){
firebase.storage().ref()
.child(item)
.getMetadata()
.then((results) => {
objOfUrl[i] = `https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media`
i++ // i ++ for objOfUrl
console.log("i is :" + i)
try{setObjUrl(objOfUrl)}catch(e){console.log("err is in view useEffect try catch e : " + e)}
})
.catch((err) => {
console.log(err);
});
console.log(objOfUrl); //console logs [0: "url.....", 1: "url....."]
}
})
}
},[displayspecificCases])
Main issue is here!
So I want to display the images in JSX. So I setState the array into a state and tried to use .map to return each item in the array so I can use img tag to display them
// inside the JSX
{objUrl.map((item, i) => { console.log(i); return <div class="column"> <img key={i} width="360px" height="270px" src={item} alt="no photo"/> </div>})}
But the problem right now is that only 1 image is displayed and not the whole array. Also whenever it renders, it seems to randomly display one of images inside the array. How do I display all images inside the array? many thanks!
sidenote: I know my code is messy, I'm still learning, please do give advice and tips on how to improve certain parts!
If I understand your code correctly it is doing:
Effect callback triggered by displayspecificCases updating
Split displayspecificCases.imageurl into array of urls
Call firebase backend for the bucket the image is in for the real image URL
Issues
You are mutating your objOfUrl state object.
objOfUrl[i] = `https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media`
You enqueue standard state updates in a loop. This enqueues all state updates using the state value from the previous render cycle (remember state is const). This is also a problem since objOfUrl IS the state reference. It should be a new array reference.
setObjUrl(objOfUrl)
I suppose this isn't a cause for your issue, but it is incorrect to use array.prototype.map to issue array side-effects.
Solution
Use a functional state update to
Correctly enqueue state updates in a loop
Correctly return a new state array reference
Code
useEffect(() => {
if (displayspecificCases.imageurl) {
displayspecificCases.imageurl.split(",").forEach((item, i) => {
console.log(item);
if (item !== "") {
firebase
.storage()
.ref()
.child(item)
.getMetadata()
.then((results) => {
// Construct complete image URL
const url = `https://firebasestorage.googleapis.com/v0/b/${
results.bucket
}/o/${encodeURIComponent(item)}?alt=media`;
console.log("i is :" + i);
try {
// Update state array at index i
setObjUrl((objOfUrl) =>
objOfUrl.map((objUrl, index) => (index === i ? url : objUrl))
);
} catch (e) {
console.log("err is in view useEffect try catch e : " + e);
}
})
.catch((err) => {
console.log(err);
});
}
});
}
}, [displayspecificCases]);
// Log state when it updates
useEffect(() => {
console.log(objOfUrl); //console logs [0: "url.....", 1: "url....."]
}, [objOfUrl]);
Solved
solution explained below
useEffect(()=>{
axios
.get("http://localhost:9998/api/v1/fault/" + lastURLSegment)
.then(async (response) => {
setDisplay(response.data)
const imagesToConstruct = await response.data.imageurl.slice(0,-1).split(",")
// console.log(imagesToConstruct)
imagesToConstruct.length > 0 && imagesToConstruct.forEach((item, i) => {
if (item !== "") {
firebase
.storage()
.ref()
.child(item)
.getMetadata()
.then((results) => { //main solution here
setImages((prevImages) => [...prevImages, `https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media`])
}).catch((err) => {
console.log(err);
});
}
})
})
}, [])
The issue
Every time useEffect is called, the constructed url is parsed into a temp array (objOfUrl) which is setState into objUrl by using setobjUrl(objOfUrl) However, for some reason, i cannot figure out why it overwrites the state and the state only ends up with one constructed URL. So i had to consult someone to help with my code and this was the solution he provided.
The solution explained
Compressed the 2 useEffect into 1 and then set new state (images) using the spread operator.
Thank you to those who have attempted to help and replied to this post!

React / ES6 - Efficiently update property of an object in an array

I am looking for the most efficient way to update a property of an object in an array using modern JavaScript. I am currently doing the following but it is way too slow so I'm looking for an approach that will speed things up. Also, to put this in context, this code is used in a Redux Saga in a react app and is called on every keystroke* a user makes when writing code in an editor.
*Ok not EVERY keystroke. I do have debounce and throttling implemented I just wanted to focus on the update but I appreciate everyone catching this :)
function* updateCode({ payload: { code, selectedFile } }) {
try {
const tempFiles = stateFiles.filter(file => file.id !== selectedFile.id);
const updatedFile = {
...selectedFile,
content: code,
};
const newFiles = [...tempFiles, updatedFile];
}
catch () {}
}
the above works but is too slow.
I have also tried using splice but I get Invariant Violation: A state mutation
const index = stateFiles.findIndex(file => file.id === selectedFile.id);
const newFiles = Array.from(stateFiles.splice(index, 1, { ...selectedFile, content: code }));
You can use Array.prototype.map in order to construct your new array:
const newFiles = stateFiles.map(file => {
if (file.id !== selectedFile.id) {
return file;
}
return {
...selectedFile,
content: code,
};
});
Also, please consider using debouncing in order not to run your code on every keystroke.

Why can't my function read the actual value of my component's state?

For a school project my group is building a table that's filled with city-data via a database-call.
The skeleton of the table-component is as such:
import React, { useState } from 'react'
function Table(props) {
const [ cities, setCities ] = useState([])
const [ pageNum, setPageNum ] = useState(0)
useEffect(()=> { // this sets up the listener for the infinite scroll
document.querySelector(".TableComponent").onscroll = () => {
if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
}
//initial fetch
fetchData(0)
},[])
async function showMoreRows() {
console.log("Show more rows!")
await fetchData(pageNum)
}
async function fetchData(page) {
// some code, describing fetching
// EDIT2 start
console.log(pageNum)
// EDIT2 end
const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
if(page) {
setCities([...cities, ...jsonResponse])
console.log("page is true", page)
setPageNum(pageNum + 1)
} else {
console.log("page is false", page) // this always runs and prints "page is false 0"
setCities([...cities, ...jsonResponse])
setPageNum(1)
}
}
return <div className="TableComponent"> { pageNum }
<!-- The rest of the component -->
</div>
}
The table features an "infinite-scrolling"-feature, so when you scroll to the bottom of the page it prints "Show more rows!" and runs fetchData(pageNum) to get more data. At this point, after the initial fetch, the pageNum-variable should be 1 or more, but for some reason the function acts as if it is 0. I put the pageNum-variable on display in the JSX, and I can see that it is 1, but it still prints out "page is false 0" when ever I run it.
When I try to google the issue, it seems the only similar thing could be that I try to read a useState-variable too soon after using setPageNum (before the redraw), but that isn't the case here as far as I can see. I give it plenty of time between tries, and it always says pageNum is zero.
Any ideas as to what I am doing wrong, and how this makes sense in any way?
Thanks for any help!
EDIT: Just tried the code I wrote over, and it seemed to work - however the full code I have doesn't work. Anyone have any ideas about problems related to this, even if the above code might work?
EDIT2: I added a console.log(pageNum) to the fetchData-function, and tested a bit, and it seems that whatever I put into the initial value in useState(VALUE) is what is being printed. That makes NO sense to me.
Help.
EDIT3: Added await, already had it in real code
EDIT4: I've tried at this for a while, but realized as I am using react that I could move the scroll-listener I have down to the JSX-area, and then it worked - for some reason. It now works. Can't really mark any answers as the correnct ones, but the problem is somewhat solved.
Thanks all who tried to help, really appreciate it.
Your staleness issues are occurring because React is not aware of your dependencies on the component state.
For example, useEffect ensures that value of showMoreRows from the scope of the initial render will be called on every scroll. This copy of showMoreRows refers to the initial value of pageNum, but that value is "frozen" in a closure along with the function and won't change when the component state does. Hence the scroll listening won't work as it needs to know the current state of pageNum.
You can resolve the issues by using callbacks to "hookify" showMoreRows and fetchData and declare their dependence on the component state. You must then declare the dependence of useEffect on these callbacks and use a clean-up function to handle the effect being invoked more than once.
It would look something like this (I haven't tried running the code):
import React, { useState } from 'react'
function Table(props) {
const [ cities, setCities ] = useState([])
const [ pageNum, setPageNum ] = useState(0)
useEffect(()=> {
// Run this only once by not declaring any dependencies
fetchData(0)
}, [])
useEffect(()=> {
// This will run whenever showMoreRows changes.
const onScroll = () => {
if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
};
document.querySelector(".TableComponent").onscroll = onScroll;
// Clean-up
return () => document.querySelector(".TableComponent").onscroll = null;
}, [showMoreRows])
const showMoreRows = React.useCallback(async function () {
console.log("Show more rows!")
await fetchData(pageNum)
}, [fetchData, pageNum]);
const fetchData = React.useCallback(async function (page) {
// some code, describing fetching
// EDIT2 start
console.log(pageNum)
// EDIT2 end
const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
if(page) {
setCities([...cities, ...jsonResponse])
console.log("page is true", page)
setPageNum(pageNum + 1)
} else {
console.log("page is false", page) // this always runs and prints "page is false 0"
setCities([...cities, ...jsonResponse])
setPageNum(1)
}
}, [setCities, cities, setPageNum, pageNum]);
return <div className="TableComponent"> { pageNum }
<!-- The rest of the component -->
</div>
}
This might not totally solve the problem (it's hard to tell without more context), but useEffect runs every render, so things like that 'initial' fetchData(0) are going to run every update, which would probably give you the result from page = 0 every time in that conditional in fetchData.
It's hard to say without more context, but I have one guess.
Try using
setCities(value => [...value, ...jsonResponse])
instead of
setCities([...cities, ...jsonResponse])
Also make sure you use await for resolving promises for requests like:
const jsonResponse = await ...
You can console log it to check if they are not pending and that you get the right property if it's a nested object.

Angular 2 Observables - Empty response on action still shows data in array

I have this function that aggregates some user data from Firebase in order to build a "friend request" view. On page load, the correct number of requests show up. When I click an "Accept" button, the correct connection request gets updated which then signals to run this function again, since the user is subscribed to it. The only problem is that once all of the friend requests are accepted, the last remaining user stays in the list and won't go away, even though they have already been accepted.
Here is the function I'm using to get the requests:
getConnectionRequests(userId) {
return this._af.database
.object(`/social/user_connection_requests/${userId}`)
// Switch to the joined observable
.switchMap((connections) => {
// Delete the properties that will throw errors when requesting
// the convo keys
delete connections['$key'];
delete connections['$exists'];
// Get an array of keys from the object returned from Firebase
let connectionKeys = Object.keys(connections);
// Iterate through the connection keys and remove
// any that have already been accepted
connectionKeys = connectionKeys.filter(connectionKey => {
if(!connections[connectionKey].accepted) {
return connectionKey;
}
})
return Observable.combineLatest(
connectionKeys.map((connectionKey => {
return this._af.database.object(`/social/users/${connectionKey}`)
}))
);
});
}
And here is the relevant code in my Angular 2 view (using Ionic 2):
ionViewDidLoad() {
// Get current user (via local storage) and get their pending requests
this.storage.get('user').then(user => {
this._connections.getConnectionRequests(user.id).subscribe(requests => {
this.requests = requests;
})
})
}
I feel I'm doing something wrong with my observable and that's why this issue is happening. Can anyone shed some light on this perhaps? Thanks in advance!
I think you nailed it in your comment. If connectionKeys is an empty array calling Observable.combineLatest is not appropriate:
import 'rxjs/add/observable/of';
if (connectionKeys.length === 0) {
return Observable.of([]);
}
return connectionKeyObservable.combineLatest(
connectionKeys.map(connectionKey =>
this._af.database.object(`/social/users/${connectionKey}`)
)
);

Categories

Resources