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.
Related
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).
I'm trying to build a simple app that lets the user type a name of a movie in a search bar, and get a list of all the movies related to that name (from an external public API).
I have a problem with the actual state updating.
If a user will type "Star", the list will show just movies with "Sta". So if the user would like to see the actual list of "Star" movies, he'd need to type "Star " (with an extra char to update the previous state).
In other words, the search query is one char behind the State.
How should it be written in React Native?
state = {
query: "",
data: []
};
searchUpdate = e => {
let query = this.state.query;
this.setState({ query: e }, () => {
if (query.length > 2) {
this.searchQuery(query.toLowerCase());
}
});
};
searchQuery = async query => {
try {
const get = await fetch(`${API.URL}/?s=${query}&${API.KEY}`);
const get2 = await get.json();
const data = get2.Search; // .Search is to get the actual array from the json
this.setState({ data });
} catch (err) {
console.log(err);
}
};
You don't have to rely on state for the query, just get the value from the event in the change handler
searchUpdate = e => {
if(e.target.value.length > 2) {
this.searchQuery(e.target.value)
}
};
You could keep state updated as well if you need to in order to maintain the value of the input correctly, but you don't need it for the search.
However, to answer what you're problem is, you are getting the value of state.query from the previous state. The first line of your searchUpdate function is getting the value of your query from the current state, which doesn't yet contain the updated value that triggered the searchUpdate function.
I don't prefer to send api call every change of letters. You should send API just when user stop typing and this can achieved by debounce function from lodash
debounce-lodash
this is the best practise and best for user and server instead of sending 10 requests in long phases
the next thing You get the value from previous state you should do API call after changing state as
const changeStateQuery = query => {
this.setState({query}, () => {
//call api call after already changing state
})
}
I am using Angular 6, ngrx/store, ngrx/effects.
I have an effect that should be triggered when i press "Save" button. I am using withLatestFrom there to collect all data what i need for sending it to the server:
#Effect({dispatch: false})
saveAll$ = this.actions$.pipe(
ofType(ActionTypes.Save),
withLatestFrom(
this.store.select(fromReducers.getData1),
this.store.select(fromReducers.getData2),
this.store.select(fromReducers.getData3),
this.store.select(fromReducers.getData4)
),
switchMap(([action, data1, data2, data3, data4]: [ActionType, Data1[], Data2[], Data3[], Data4[]]) => {
// here is some operations with these data
return this.apiService.saveData({data1, data2, data3, data4})
})
)
Here is getData1 selector:
export const getData1= createSelector(
getItems,
getIndexes,
(items, indexes) => {
console.log('HI, I AM getData1');
return transformItems(items, indexes);
}
);
getItems, in turn, return state.items. The problem is that state.items can be modified in another effect:
#Effect()
handleItemsChanges$ = this.actions$.pipe(
ofType(ActionTypes.ChangesInItems),
withLatestFrom(
this.store.select(fromReducers.getItems),
this.store.select(fromReducers.getUsers),
),
switchMap(([action, items, users]: [ActionType, Item[], User[]]) => {
console.log('I AM handleItemsChanges');
const actions = [];
if (itemsShouldBeUpdated) {
actions.push(new UpdateData(changes))
}
})
)
So getData1 selector gets data from the store depend on another effect named handleItemsChanges. handleItemsChanges effect is triggered every time something is changed related to the items and recalc it again.
As a result, in saveAll i am getting not actual state.items.
What am i doing wrong? May be i should use another operator insted of withLatestFrom or what ca be the solution? Thank you
P.S. Btw i am using withLatestFrom every time when i want to get some data from the store. Is it correct?
you need to have action handleItemsChanges fired before saveAll gets fired. One way to do it is to create an effect on handleItemsChanges action and trigger the save action.
The framework will guarantee the order of execution (handleItemsChanges first then save), this way the withLatestFrom operation will work as you expected.
I've found discussion on ngrx github : https://github.com/ngrx/platform/issues/467
Looks like we have 2 ugly variants for accessing store from effects now.
I'm using this Gumroad-API npm package in order to fetch data from an external service (Gumroad). Unfortunately, it seems to use a .then() construct which can get a little unwieldy as you will find out below:
This is my meteor method:
Meteor.methods({
fetchGumroadData: () => {
const Gumroad = Meteor.npmRequire('gumroad-api');
let gumroad = new Gumroad({ token: Meteor.settings.gumroadAccessKey });
let before = "2099-12-04";
let after = "2014-12-04";
let page = 1;
let sales = [];
// Recursively defined to continue fetching the next page if it exists
let doThisAfterResponse = (response) => {
sales.push(response.sales);
if (response.next_page_url) {
page = page + 1;
gumroad.listSales(after, before, page).then(doThisAfterResponse);
} else {
let finalArray = R.unnest(sales);
console.log('result array length: ' + finalArray.length);
Meteor.call('insertSales', finalArray);
console.log('FINISHED');
}
}
gumroad.listSales(after, before, page).then(doThisAfterResponse); // run
}
});
Since the NPM package exposes the Gumorad API using something like this:
gumroad.listSales(after, before, page).then(callback)
I decided to do it recursively in order to grab all pages of data.
Let me try to re-cap what is happening here:
The journey starts on the last line of the code shown above.
The initial page is fetched, and doThisAfterResponse() is run for the first time.
We first dump the returned data into our sales array, and then we check if the response has given us a link to the next page (as an indication as to whether or not we're on the final page).
If so, we increment our page count and we make the API call again with the same function to handle the response again.
If not, this means we're at our final page. Now it's time to format the data using R.unnest and finally insert the finalArray of data into our database.
But a funny thing happens here. The entire execution halts at the Meteor.call() and I don't even get an error output to the server logs.
I even tried switching out the Meteor.call() for a simple: Sales.insert({text: 'testing'}) but the exact same behaviour is observed.
What I really need to do is to fetch the information and then store it into the database on the server. How can I make that happen?
EDIT: Please also see this other (much more simplified) SO question I made:
Calling a Meteor Method inside a Promise Callback [Halting w/o Error]
I ended up ditching the NPM package and writing my own API call. I could never figure out how to make my call inside the .then(). Here's the code:
fetchGumroadData: () => {
let sales = [];
const fetchData = (page = 1) => {
let options = {
data: {
access_token: Meteor.settings.gumroadAccessKey,
before: '2099-12-04',
after: '2014-12-04',
page: page,
}
};
HTTP.call('GET', 'https://api.gumroad.com/v2/sales', options, (err,res) => {
if (err) { // API call failed
console.log(err);
throw err;
} else { // API call successful
sales.push(...res.data.sales);
res.data.next_page_url ? fetchData(page + 1) : Meteor.call('addSalesFromAPI', sales);
}
});
};
fetchData(); // run the function to fetch data recursively
}
At a loss on this one.
I'm using Ember and Ember data. I've got this extra implementation of ic-ajax to make GET, POST and PUT calls. Anyway, i'm trying to make a GET call then turn those results into model instances.
return this.GET('/editor')
.then((data) => {
return data.drafts.map((draftData) => {
let draft = this.store.find('draft',draftData.id);
console.log(draft.get('type'));
return draft;
});
});
My API returns proper data as data.drafts. This map is supposed to return an array of promises that resolve to draft models. It does not. It resolves to a draft model that has id, date, and title. But that's it. I have 25 others attributions.
In another part of the application i'm getting drafts using findAll on the model. And those models look fine. But when I try store.findRecord('draft',id) i get these fake objects.
-- edit
This is what my ReOpenClass method looks like for getting an array of objects from the server and turning them into ember objects
search(critera) {
let query = { search: critera };
let adapter = this.store.adapterFor('application');
let url = adapter.buildURL('article','search');
return adapter.ajax(url,'GET', { data: query }).then(response => {
let articleRecords = response.articles.map((article) => {
let record;
try {
record = this.store.createRecord('article', article);
} catch(e) {
record = this.store.peekRecord('article', article.id);
}
return record;
});
return articleRecords;
});
},
So far I can't find a better way to pull this off.