State objects and child component using sub values of object - javascript

I have a Dropzone that allows for multiple concurrent uploads and I want to show the progress for all uploads.
In my Dropzone component I have part of the state which is an array of uploads:
const [uploads, setUploads] = useState([])
Each element of the uploads array will be an upload object that has a few values, like so:
const uploading = {
id: 1,
files: <array of files>,
progress: 0
}
Once files/folders are dropped into the dropzone, the "uploading" object will be added to the "uploads state array" and the files will be sent to the backend API, which asynchronously uploads the files to the server.
The backend will periodically send a progress callback to the UI, to update the progress value of the correct element in the uploads state array (see progressCallback below)
What I am currently unable to achieve is to make sure the UI re-renders every time an object in the uploads array is being updated to show progress, so that I can show the progress of all uploads as they happen.
The UI Component looks like this:
export function UploaderDropzone(props) {
const [uploads, setUploads] = useState([])
const progressCallback = useCallback((progressObject, sessionContext, step) => {
const {uploadSessionParameters} = sessionContext
let uploading = {}
// let tmpArray = []
const tmpArray = [...uploads]
if (step === 'progress') {
const filtered = findUploadById(tmpArray, uploadSessionParameters.uploadSessionId)
uploading = filtered[0]
if (uploading) {
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)
uploading.progress = progressObject.percentUpload
tmpArray.push(uploading)
setUploads(tmpArray)
// setUploads(prevUploads => [...prevUploads, uploading])
}
console.log('progress tmpArray = ' + JSON.stringify(tmpArray));
console.log('progress uploads = ' + JSON.stringify(uploads))
}
if (step === 'initialize') {
const uploadNumber = uploads.length + 1
uploading = {
uploadSessionId: uploadSessionParameters.uploadSessionId,
files: sessionContext.files,
uploadNumber: uploadNumber,
uploadName: `Upload #${uploadNumber}`,
sent: false,
progress: 0,
}
tmpArray.push(uploading)
setUploads(tmpArray)
console.log('initialize tmpArray = ' + JSON.stringify(tmpArray))
console.log('initialize uploads = ' + JSON.stringify(uploads))
}
}, [uploads])
const progressBars = uploads.map((upload) => {
return (
<Fragment>
<ProgessBar progress={upload.progress} />
</Fragment>
)
})
// ... more code here ... not required for understanding
return {
<Fragment>
<Dropzone
onDrop={
acceptedFiles => {
const filteredFiles = acceptedFiles.filter((file) =>
validateFile(file))
console.log("Filtered files" + filteredFiles)
if (filteredFiles.length > 0) {
setAlertMsg('')
}
else {
setAlertMsg('No files uploaded.')
}
// call to Node.js backend, passing it the progressCallback
startAsyncUploadSession(filteredFiles, progressCallback);
}
}
/>
{progressBars}
</Fragment>
}
}
The ProgressBar component is very simple:
export function ProgressBar(props) {
const {progress} = props
return (
<Fragment>
<p>`${progress}% uploaded ...`</p>
</Fragment>
)
}
Right now, this code doesn't even show the progress bar even though the uploads state array is constantly being updated in the progressCallback. Since I don't know the number of concurrent uploads that will be done, I cannot set the state in the higher order component and pass it as props, I need the child component (ProgressBar) to receive it as props from the multiple objects in the state array ... but I am clearly missing something ...
Any pointers ? Any hooks I could use to register the progress value of the objects in the uploads state array so that every time the backend updates us on the progress it is reflected in the UI ?
Edit: To include the partial fix suggested by #Robin Zigmond
Edit2: After some debugging, it seems to be a synchronization issue. So I need to add some code and details here.
When files are dropped into the Dropzone, its sends the files to the Node.js backend through a function call to a mock server, the call to startAsyncUploadSession(filteredFiles, progressCallback); in the onDrop event of the Dropzone (which uses the react-dropzone lib).
It would seem that when I call progressCallback later, the state is as it was on the first render, aka uploads state array is an empty array as it was when the files were dropped, not the updated one which contains the object added to uploads array in the 'initializing' step.
So amended question would be "How to make sure that the UI state is up to date when the progressCallback is called later on by the backend ?"

The problem is in your state updating code inside progressCallback. Here is the offending code, for reference:
const tmpArray = uploads
const itemIndex = tmpArray.indexOf(uploading)
tmpArray.splice(itemIndex, 1)
// HERE UPDATING ONE OF ITEM'S VALUES IN UPLOADS STATE ARRAY
uploading.progress = progressObject.percentUpload
tmpArray.push(uploading)
setUploads(tmpArray)
What this does is:
sets tmpArray to be a reference to the same object (uploads) as the current state
then mutates that array, first by splicing an element out, then pushing a new element on to it
At no point in step 2) does the reference change. So when you then call setUploads(tmpArray) - which might as well be setUploads(uploads) as those two variables are still references to the exact same array - React thinks you're setting the state to be exactly what it was, and therefore doesn't know to rerender.
That's the long way of explaining why you should never mutate state, as you are doing here. You need to update it immutably - that is, leave the old state alone, construct a new object/array, and pass that to the function that sets the new state.
There are a number of ways to do that here, but in your case it should be as simple as just making tmpArray a (shallow) *copy) of the current state. That is, change:
const tmpArray = uploads
to
const tmpArray = [...uploads]
Now tmpArray is a new reference, to an array holding the same values as before. Note that the copy is only "shallow", so the objects inside the array are still references to just one underlying object for each array element. But that doesn't seem to matter here, because you don't mutate those objects. If you try your code with this change, I believe it should start to work (or at least get you past this particular problem).

Related

IndexedDB syncronism issue when creating parallel stores in the same db

Whenever I try to archieve creating diffrent stores in the same database at the same time only one of them is created. Is there a way to resolve this syncronism issue.
I've already been able to solve this issue but I'll clarify what I ment in case it might help someone else. I have a GridView component which is mapped multiple times. This component saves the columns, how they are aranged and their specific behaviors to be stored inside indexedDB. The issue I had was that I used a function that created a Db named after the page and the stores (one for each GridView inside the same DB). In order to create all the stores at the same time (in this case I had to create 9 of them) I had to trigger a version change for each of the new stores in order to be able to persist the information. Inside the function I searched for the Db actual version and added 1 to trigger the version change event. The problem has that because they where searching the version synchronously inside this function all of the itterations were getting the same version and the result would be that only the first store was beeing created because only the first iteration of the map would trigger a version change. In order to resolve this issue I used index prop iside the map function and passed it as an order prop to my GridView component. Then instead of triggering the version change (version+1) inside the function I triggered by using version+order, this way all the stores where being created because it assures that all the versions were going to be higher than the previous ones.
I'll give some code to maybe help the explanation.
This is the map:
{status.map((status, index) => {
return (
<GridView
pageName={"Page"} // DB Name
gridName={status} // Store Name
order={index + 1} // index + 1 so that the order is never 0
//all the other props...
/>
);
})}
Inside the GridView component I have a function that triggers on first render to search for the store inside the db and if there is no information inside creates and then fills the store with the information needed. This the function:
/**
*
#param {String} DB_NAME
#param {String} STORE_NAME
#param {String} keyPath
#param {Int} order
#param {Object/Array} info
#returns {Object}: resolve: {success,message,storeInfo), reject:{error, message}
*/
const createAndPopulateStore = (
DB_NAME,
STORE_NAME,
keyPath,
order = 1,
info
) => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME);
request.onsuccess = function (e) {
let database = e.target.result;
let version = parseInt(database.version);
database.close();
//This was the critical part in order to create multiple stores at the same time.
let secondRequest = indexedDB.open(DB_NAME, version + order);
secondRequest.onupgradeneeded = (e) => {
let database = e.target.result;
//Early return if the store already exist.
if (database.objectStoreNames.contains(STORE_NAME)) {
reject({
success: false,
message: `There is already a store named: ${STORE_NAME} created in the database: ${DB_NAME}`,
});
return;
}
let objectStore = database.createObjectStore(STORE_NAME, { keyPath });
if (info) {
// Populates the store within the db named after the gridName prop with the indexedColumns array.
if (Array.isArray(info)) {
info.map((item) => objectStore.put(item));
} else {
Object.entries(info).map((item) => objectStore.put(item));
}
}
};
secondRequest.onsuccess = function (e) {
resolve({
success: true,
message: `Store: ${STORE_NAME}, created successfully.`,
storeInfo: info ?? {},
});
let database = e.target.result;
database.close();
};
};
});
};
I hope this will help! Fell free to ask any questions regarding this issue and I'll try to answer them as soon as I can.

Array is sometimes recreated, and in other times modified

I am trying to implement a multiple image file selector in my React app.
This is my input element:
<input
type="file"
multiple
onChange={handleImageChange}
/>
{renderPhotos(venueImages)}
These are the functions that are called when files are chosen:
const [venueImages, setVenueImages] = useState([]);`
const renderPhotos = source => {
console.log(source); ////////log 1
return source.map(photo => {
return <img src={photo} key={photo} />;
});
};
const handleImageChange = e => {
if (e.target.files) {
const filesArray = Array.from(e.target.files);
console.log(filesArray); ///////// log 2
filesArray.forEach(file => {
const tempUrl = URL.createObjectURL(file);
console.log(tempUrl); ////// log 3
setVenueImages([...venueImages, tempUrl]);
});
}
};
I call renderPhotos to show a preview off all the selected photos before uploading.
The issue I'm facing is as follow:
If I choose, for example, 5 photos, only 1 would end up being rendered on screen.
I've inserted console logs in handleImageChange, and what I get logged is confusing me even more.
The second log (I've numbered them in my code) prints an array of 5 files.
After from log 3 that I'll get 5 logs of the newly generated temporary URLs for each of the files.
But log 1, would only get printed once.
Now - if I'll click the input element to choose more files, I'll end up with another rendered image.
So basically everytime I choose images, no matter how many I've chosen, I'll only get one more image rendered.
The problem is that you are referencing the venueImages array in your setVenueImages call. Because the state update performed by setVenueImages is asynchronous, then you cannot be certain that venueImages contains all of the previous state updates.
The set state function can be passed a function that takes the old value, instead of just passing it the new value. This will ensure that your state updates are sequential. Replace your setVenueImages call with this:
setVenueImages(prevImages => [...prevImages, tempUrl]);
An additional change that I will suggest is to perform a concatenation of all images, instead of adding them one by one. This should be faster.
const handleImageChange = e => {
if (e.target.files) {
const filesArray = Array.from(e.target.files).map(file => URL.createObjectURL(file));
console.log(filesArray); ///////// log 2
setVenueImages(prevImages => prevImages.concat(filesArray));
}
};
That is happening because when you are saving the tempUrl, only one url is getting saved. Also do not set the state by adding images one by one.
Updated version of your handleImageChange function can be
const handleImageChange = e => {
if (e.target.files) {
const filesArray = Array.from(e.target.files);
const tempUrls = filesArray.map(file => URL.createObjectURL(file)))
setVenueImages([...venueImages, ...tempUrls])
}
};

Bind event handler to document & have access to firebase api data via useEffect

Quick version:
My ultimate goal is to do something like the link below but with an async call to firebase per useEffect where the list data is composed of firebase object content.
https://codesandbox.io/s/usage-pxfy7
Problem
In the code below useEffect encapsulates code that pings firebase and gets some data back called "clients". The data is retrieved perfectly.
I then store that data using useState to two different instances of useState. The data is stored at clientList and clientListForRender.
So far so good.
Now the problem starts.
I have a third instance of useState that takes a number. I want to set a keypress event to the document so that I can use the up/down arrows to toggle the counter and access each value of the clientListForRender array.
When I set the eventListener I do not have access to the array (presumably due to the async calls not being in an order that allows for it).
I am not sure how to write my hooks in a way that gives me the result I want.
Thank you.
const clientsRef = firebase.database().ref('clients');
const [clientList,setClientListState] = useState([]);
const [clientListForRender,setClientListStateForRender] = useState([]);
const [selectedIndex, updateSelectedIndex] = useState(0);
useEffect(() => {
function handleKeyPress(event,arr){
console.log(arr)
if(event.key === "ArrowDown"){
updateSelectedIndex((prev)=>{
return prev += 1
});
}
}
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key; // __________________________1. get firebase data
setClientListState(function(prev){
setClientListStateForRender(()=>[client,...prev]); //_______2 store data
// document.addEventListener('keydown', handleKeyPress); <---I am not sure where to put this. I have experimented and
// I decided to omit my cluttered "experiments" to protect your eyes
return[client,...prev]
});
});
},[]);
Ok there are few issues with the code you posted:
1) You should definitely not add your keyboard listener in the child_ added listener ( this means that every time the child_added listener is called, you are going to create a new listener, leading to unexpected results and memory leak)
2) You are calling setState in a setState updater function (the callback function you provided for, setClientListState), which is an anti pattern and makes your code hard to follow and understand, and will cause unexpected effects once the component grows. If you want to update a state based on a previous state then use the useEffect callback
3) the useEffect function takes a second parameter, called array of dependencies. When you have provided it with an empty array, it means that you want your effect to run only once, which is problematic because we see that the function depends on clientsRef variable. ( from this actually comes your problem because the keyboard listener was having the old value of your clientsList which is the empty array, and so it was always returning 0, when keys where pressed, i explained more in the code sandbox)
4)You should return a callback function from the useEffect function to clean the effects you created, turning off the listeners you attached (or else you might have memory leaks depending on how much the component gets mounted/unmounted)
ok here is how the code should be to work:
const clientsRef = firebase.database().ref('clients');
const [clientList, setClientListState] = useState([]);
// I don't understand why you wanted another list, so for now i only use on list
// const [clientListForRender,setClientListStateForRender] = useState([]);
const [selectedIndex, updateSelectedIndex] = useState(0);
useEffect(() => {
function handleKeyPress(event, arr) {
if (event.key === 'ArrowDown') {
updateSelectedIndex(prev => {
if (prev >= clientList.length - 1) {
return (prev = 0);
} else {
return prev + 1;
}
});
}
}
clientsRef.on('child_added', snapshot => {
const client = snapshot.val();
client.key = snapshot.key; // __________________________1. get firebase data
setClientListState(function(prev) {
return [client, ...prev];
});
});
document.addEventListener('keydown', handleKeyPress);
// here you should return a callback to clear/clean your effects
return () => {
document.removeEventListener('keydown', handleKeyPress);
clientsRef.off();
};
// Its important to add these here, or else each time your keyboard listener runs it will have the initial value of
// clientsList ([]), and so clientsList.length = 0, and so you will always updateSelectedIndex(0)
}, [clientList, clientsRef]);
//here render based on selected list as you wish
Finally i have set up a working codesandbox that emulated data fetching based on the example you give https://codesandbox.io/s/usage-4sn92, i added some comments there to help explain what i said above.

node.js server is hit multiple times into async function in React

I'm working with React, MongoDB, node.js and Express and this is my situation:
I have this piece of code inside my component:
renderWishlist(){
var quantity;
var itemID;
var tmp;
var myData = this.props.currentCart;
// console.log(myData.length) returns 3
for (var k=0; k<myData.length; k++){
tmp = myData[k];
quantity = tmp.quantity;
itemID = tmp.itemID;
this.props.fetchBook(itemID).then(function(book){
console.log("book: "+JSON.stringify(book));
});
}
}
myData is an object which holds a bunch of books info.
As you can see from my code above I'm iterating through all these books, retrieving the ID of the book and the available quantity, then I try to get other information (price, pages, etc...) for that particular book from another collection inside the same MongoDB database.
Once this piece of code is running I keep getting multiple logs like this inside chrome console:
book: {"type":"fetch_book","payload":{"data":{"_id":"58f6138d734d1d3b89bbbe31","chef":"Heinz von Holzen","title":"A New Approach to Indonesian Cooking","pages":"132","price":23,"image":"IndonesianCooking"},"status":200,"statusText":"OK","headers":{"content-type":"application/json; charset=utf-8","cache-control":"no-cache"},"config":{"transformRequest":{},"transformResponse":{},"timeout":0,"xsrfCookieName":"XSRF-TOKEN","xsrfHeaderName":"X-XSRF-TOKEN","maxContentLength":-1,"headers":{"Accept":"application/json, text/plain, */*"},"method":"get","url":"http://localhost:3001/books/58f6138d734d1d3b89bbbe31"},"request":{}}}
Which retrieves correctly the book but it seems to hit the server multiple times for the same book and I don't know why.
For completeness this is fetchBook action creator:
export function fetchBook(id){
const request = axios.get('http://localhost:3001/books/'+id);
return {
type: FETCH_BOOK,
payload: request
};
}
The reducer:
import {FETCH_BOOKS, FETCH_BOOK,FETCH_WISHLIST} from '../actions/types';
const INITIAL_STATE = { myBooks:[], currentCart:[], currentBook:[] };
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_BOOKS:
return { ...state, myBooks:action.payload };
case FETCH_BOOK:
return { ...state, currentBook:action.payload };
case FETCH_WISHLIST:
return { ...state, currentCart: action.payload };
default:
return state;
}
}
My node.js server call:
router.get('/books/:id', function(req, res, next){
Book.findById({_id:req.params.id}).then(function(book){
res.send(book);
}).catch(next);
});
Why the server is hit multiple times? If I have, let's say 3 books inside myData, I'd expect the server to be hit only 3 times.
Another question I have is: How can I make the for loop to wait for fetchBook action to finish before going on iterating the next myData item?
You say renderWishList is called from the render() method. Your call to this.props.fetchBook(itemID) is updating your state, which triggers a re-render, which calls this.props.fetchBook(itemID) and ad infinitum it goes. You can put a console.log() at the start of your render() method to confirm this.
I would call renderWishList() from your constructor() or your componentDidMount() method. And I would rename it to something like createWishList(), because you are not rendering it with this function, but creating the list which needs to be rendered.
Next, you will want to make sure you are updating your state correctly every time your call to fetchBook returns, and then you'll want to use that to render correctly.
Update State
I would change the FETCH_BOOKS reducer to:
case FETCH_BOOK:
return {
...state,
myBooks: [
...state.myBooks,
action.payload
]
};
This will add the book just fetched to the end of the array of book objects myBooks. I am not clear on what is books vs wishlist, so you may want to change the names I've used here. The idea is that when each loop of the for loop is done, your myBooks array in your state has each book that was passed in from this.props.currentCart.
Note, I'm not sure, but you may need to execute a dispatch inside the .then of your this.props.fetchBooks() call.
Render with State
I'm not sure how your are accessing your state, but probably you then want to take your state.myBooks, and map it to create a separate line item, which you can use in your render method. You do this by defining a const like this at the top of your render method:
const mappedBooks = state.myBooks.map(book =>
<div>Title: {book.title}</div>
);
You can then use {mappedBooks} in the return() of your render method where you want a list of the books in myBooks to show on the screen.
Async For Loop
Last, I wouldn't worry that you are running each fetchBook asynchronously. In fact, this is good. If you implement it so that each response updates the state, as I've suggested, then that will trigger a re-render each time and your screen will load with each book. Of course with a few books it will happen so fast it won't matter. But this is a very "React-y" way for it to work.
I can't figure out why your server is hit multiple times. But you can use bluebird npm for your second question. reduce function of bluebird will do exactly what you want.
You can see the documentation here: http://bluebirdjs.com/docs/api/promise.reduce.html
renderWishlist() {
var bluebird = require('bluebird')
// myData is an array
var myData = this.props.currentCart;
bluebird.reduce(myData, function(value, tmp) {
var quantity = tmp.quantity;
var itemID = tmp.itemID;
return this.props.fetchBook(itemID).then(function(book) {
console.log("book: "+JSON.stringify(book));
});
}, 0).then(function() {
// called when myData is iterated completely
});
}
This code should work for you.

How to reactively execute code as soon as a client Collection grows?

I'm trying to keep track of an increment of a certain reactive value in Meteor. If the current value has increased by 1 or more, I want something to happen. I do have two problems:
First: I don't know how I can make an if-statement of this function.
Second: I don't know how I can keep track of the increases.
This is the code I have now, using the Mongo.Collection cars (which is from an API):
api = DDP.connect('url');
const currentCars = new Meteor.Collection('cars', api);
const newCars = cars.find().count()
if (Meteor.isClient) {
Template.currentCars.helpers({
carsInCity: function() {
return currentCars.find(
{
location: "city"
}).count();
},
})
}
So there's a current amount of cars in the city. Everytime when there is one more car, I want something to happen in the code. But how on earth can I do that? Maybe by keeping track of when the database has been updated?
A fairly straight-forward solution would be to store the current amount of data in that collection, then run a reactive computation to see if anything changed.
Something like this:
let currentCarsCount = cars.find().count()
Tracker.autorun(function checkForAddedCars() {
// cars.find() is our reactive source
const newCarsCount = cars.find().count()
if(newCarsCount > currentCarsCount) {
currentCarsCount = newCarsCount
// There's new cars, handle them now
// ...
}
})
You may also want to use a template-level autorun so that you don't have to manage stopping checkForAddedCars. You could also store currentCarsCount as a state on the template instance instead of as a hoisted loner.
For example:
Template.currentCars.onCreated(function() {
const templateInstance = this;
// equivalent:
const templateInstance = Template.instance();
templateInstance.currentCarsCount = cars.find().count();
templateInstance.autorun(function checkForAddedCars() {
// cars.find() is our reactive source
const newCarsCount = cars.find().count();
if(newCarsCount > templateInstance.currentCarsCount) {
templateInstance.currentCarsCount = newCarsCount;
// There's new cars, handle them now
// ...
}
});
});
It would also allow you to access currentCarsCount from other places in the template code.

Categories