Optimize Repeated (Same) API Calls - React - javascript

Scenario -
Suppose, there is an API which gives a list of books.
[ { id:1,
name: "A",
author: "https://author/1"
},
{ id:2,
name: "B",
author: "https://author/10"
},
{ id:3,
name: "A",
author: "https://author/3"
},
{ id:4,
name: "A",
author: "https://author/1"
}
...
]
Now, I need to call the author API to get the author details, but I need to avoid calling those APIs which has already been called.
I tried doing it by keeping a state object and updating the object whenever a new api is called. If the API URL already has an entry in the object, that call is not repeated.
However, I suspect because of async nature of setState, the update to the state Object are clubbed and in the subsequent iteration, when I check the object to find the previous entry, it doesn't reflect.
...
const[authorDetail, setAuthorDetail] = useState({});
useEffect(() => {
for(let i=0;i<books.length;i++)
{
if(!authorDetail[books[i].author]) {
// make API call to fetch author detail
authorDetail[books[i].author] = "called"
setState({...authorDetail});
}
});
How can I optimise the API call for this case.
For example, if I have 7 Harry Potter books, I would like to make only one call to fetch J.K. Rowling's data.

I believe the problem here is that your effect is maintaining a reference to the old state object. I think the easiest way to solve it will be by using a ref object.
const authorDetail = useRef({});
useEffect(() => {
for (let i = 0;i < books.length; i++) {
if (!authorDetail.current[books[i].author]) {
// make API call to fetch author detail
authorDetail.current[books[i].author] = "called"
}
}
}, [books]); // note the dependencies here!
books is a dependency for the effect, but you could also provide empty square brackets ([]) which will force it to only run when the component mounts.

Related

Is there a more accurate way to use <MockedProvider /> for testing apollo requests

I've got my <MockedProvider /> set up passing in mocks={mocks}. everything is working, all good.
the issue is I have a form that whenever any part of it is edited then this makes a mutation, which returns a response and updates the total. say for example, quantity is changed, mutation increases quantity from 1 to 2. total price should double
problem is that in unit tests and mocked provider you only test the functionality in props and hardcoded response. it's not a proper test. perhaps it's more of an e2e/integration test but I was wondering if there's anything you can do with MockedProvider that allows for better testing in this situation?
Instead of using the normal static result property of the objects in the mocks array, you can set a newData function that will run dynamically and use whatever is returned as the result value. For example:
let totalNoteCount = 0;
const mocks = [{
request: {
query: CREATE_NOTE,
variables: {
title: 'Aloha!',
content: 'This is a note ...',
},
},
newData: () => {
// do something dynamic before returning your data ...
totalNoteCount += 1;
return {
data: {
createNote: {
id: 1,
totalNoteCount,
},
},
};
}
}];

Sync + Async POSTing nested parent-children objects to preserve the order

I have a nested array of Person objects.
Each Person object has a mandatory name. Each Person can also optionally have a children field that contains an array of other Person objects (that also have a children field - so the "depth" of the family tree can essentially go on forever.)
If there is no children, the children field will just be an empty array [].
E.g.
const family_name = "The Numbers";
const family = [{
name: "1",
children: [],
},
{
name: "2",
children: [{
name: "2-1",
children: [{
name: "2-1-1",
children: [],
}, ],
},
{
name: "2-2",
children: [],
}
],
},
{
name: "3",
children: [{
name: "3-1",
children: [],
}, ],
},
]
I need to POST the "parent" before the "child". When I POST a Person, I get its id back in the response.data. This id needs to be used in the immediate child's POST as a parent_id so that child will be associated to the parent.
The topmost Person will need to have their parent_id be the family_name.
Each "level" needs to be POSTed asynchronously as my back-end needs to preserve the order. (Note: Calculating the order of the Person on the client-side and passing that value to the back-end is not a solution as Person is actually a MPTT model where the insertion order matters.)
E.g. 1 then 2 then 3
E.g. 2 then 2-1 then 2-2.
However, the nested Persons can be POSTed in sync. For example, once POSTing 2 returns with a 201 response, its "sibling" 3 and its "child" 2-1 can be POSTed at the same time.
How can I optimally POST all Persons in a nested array so that the order is preserved? Please note that I am using axios.
Edit: Here is some pseudo-code:
function postPersons(persons, parent_id) {
// Async POST all objects in persons
// e.g. POST 1 then 2 then 3 to family_name
// For each successful POST, if person has children,
// async POST those children to that person
// e.g. Once POST to 2 resolves, POST 2-1 then 2-2 to 2
// e.g. Once POST to 3 resolves, POST 3-1 to 3
// Repeat for all successful POSTs
// e.g. Once POST to 2-1 resolves, POST 2-1-1 to 2-1
}
postPersons(family, family_name)
Instead of using async/await for a sequential loop, I'd recommend to use a synchronous loop and accumulate two things separately:
the promise for the child that is processed sequentially (see here or there)
an array of promises for all the recursive calls, that will be Promise.all'd in the end
This ensures proper order as well as immediate propagation of errors.
So the code will look like
function postPersons(persons, parent_id) {
const results = [];
let chain = Promise.resolve();
for (const person of persons) {
chain = chain.then(() =>
postSinglePerson(person, parent_id)
);
results.push(chain.then(result =>
postPersons(person.children, result.id)
));
}
return Promise.all(results);
}
postPersons(family, family_name).then(() => console.log('Done'), console.error);

Array - What's the best way to check a value exists in one of the array elements (React / JS)

So this is an example data array that I will get back from backend. There are a few use cases as shown below and I want to target based on the subscription values in the array.
Example: 1
const orgList = [
{ id: "1", orgName: "Organization 1", subscription: "free" },
{ id: "2", orgName: "Organization 2", subscription: "business" },
];
In the example 1 - when array comes back with this combination - there will be some styling and text to target the element with subscription: free to upgrade its subscription
Example 2:
const orgList = [
{ id: "1", orgName: "Organization 1a", subscription: "pro" },
{ id: "2", orgName: "Organization 2a", subscription: "business" },
];
Example 3:
const orgList = [
{ id: "1", orgName: "Organization 1b", subscription: "free" },
];
In the example 3 - when array comes back with only one element - there will be some styling and text to target the element say to upgrade its subscription
At the moment, I'm simply using map to go over the array that I get back like so:
{orgList.map((org) => (...do something here)} but with this I'm a bit limited as I don't think this is the best way to handle the 3 use cases / examples above.
Another idea is too do something like this before mapping but this:
const freeSubAndBusinessSub = org.some(org => org.subscription === 'free' && org.subscription === "business")
but doesn't seem to work as it returns false and then I'm stuck and not sure how to proceed after..
So my question is what's the best way to approach this kind of array to target what do to with the elements based on their values?
You mention that using .map() is limited, but you don't expand on it. Logically what it sounds like you want is a separate list for each type to act upon. You can accomplish this using .filter() or .reduce(), however, in this case .map() is your friend.
// Example 1
const free = orgList.filter(org => org.subscription === 'free');
const business = orgList.filter(org => org.subscription === 'business');
free.map(org => /* do free stuff */);
business.map(org => /* do business stuff */);
// Example 2
const subscriptions = orgList.reduce((all, cur) => {
if (!all.hasOwnProperty(cur.subscription)) {
all[cur.subscription] = [];
}
all[cur.subscription].push(cur);
return all;
}, {});
subscriptions['free'].map(org => /* do free stuff */);
subscriptions['business'].map(org => /* do business stuff */);
// Example 3
orgList.map(org => {
switch(org.subscription) {
case 'free':
/* do free stuff */
break;
case 'business':
/* do business stuff */
break;
}
})
You'll notice that in all the examples, you still need to map on the individual orgs to perform your actions. Additionally, with the first two examples, you'll be touching each element more than once, which can be incredibly inefficient. With a single .map() solution, you touch each element of the list only once. If you feel that you do free stuff actions become unwieldy, you can separate them out in separate functions.

Construct objects for client in Firebase Cloud Functions

I'm working on a simple registration system using Firebase as a backend. I am successfully authenticating users and writing to the database. I have an index of courses and users with the following structure:
{
courses: { // index all the courses available
key1: {
title: "Course 1",
desc: "This is a description string.",
date: { 2018-01-01 12:00:00Z }
members: {
user1: true
...
}
},
key2 { ... },
},
users: { // track individual user registrations
user1: {
key1: true,
...
},
user2: { ... }
}
}
I have a cloud function that watches for the user to add a course and it builds an array with the corresponding courseId that will look at the courses node to return the appropriate items.
exports.listenForUserClasses = functions.database.ref('users/{userId}')
.onWrite(event => {
var userCourses = [];
var ref = functions.database.ref('users/{userId}');
for(var i=0; i<ref.length; i++) {
userCourses.push(ref[i])
}
console.log(userCourses); // an array of ids under the user's node
});
So, my question has two parts:
How can I build the updated object when the page is loaded?
How do I return the function to the client script?
Question 1: From the client side you want to get the reference to the database path. Then you want to call the child_added event. Keep it in-memory, this will be called whenever one is add then you can update your UI.
var ref = db.ref("path/to/courses");
ref.on("child_added", function(snapshot, prevChildKey) {
var newClass = snapshot.val();
});
If you are completely refreshing the page then you can always grab the data again from the database path by using the value option and calling once
Questions 2: You don't. This is considered an asynchronous function. If you wanted a response from the function then you would setup an HTTP trigger and wait for the response from that function.

Can't get state to update array inside of an object correctly in REACT/REDUX

I am going to break this down step by step for what I want to happen so hopefully people can understand what I am wanting.
Using React/Redux, Lodash
I have many post that are sent from a back end api as an array. Each post has an _id. When I call on the action getAllPost() it gives me back that array with all the post. This is working just fine.
I then dispatch type GET_ALL_POSTS and it triggers the reducer reducer_posts to change/update the state.
reducer:
export default function(state = {}, action) {
switch(action.type) {
case GET_ALL_POSTS:
const postsState = _.mapKeys(action.payload.data, '_id');
//const newPostsState = _.map(postsState, post => {
//const newComments = _.mapKeys(post.comments, '_id');
//});
return postsState;
break;
default:
return state;
break;
}
}
As you can see I change the array into one giant object that contains many post as objects with keys that are equal to their '_id'. This works just fine and returning this part of the state also works fine.
As I mentioned each of these posts has a comments value that is an array. I would like to change the comments array into one large object that holds each comment as an object with a key that is equal to their '_id' just like I did in the post.
Now I need to do this all at once and return the newly created state with One large object that contains all the post as objects and on each of those post there should be a comments object that contains all the comments as objects. I will try to write some example code to show what I am trying to do.
Example:
BigPostsObject {
1: SinglePostObject{},
2: SinglePostObject{},
3: SinglePostObject {
_id: '3',
author: 'Mike',
comments: BigCommentObject{1: SingleCommentObject{}, 2: SingleCommentObject{}}
}
}
I hope that the example kind of clears up what I am trying to do. If it still is confusing as to what I am doing then please ask and also please do not say things like use an array instead. I know I can use an array, but that is not helpful to this post as if others want to do it this way that is not helpful information.
Write a function that processes all the comments from the comments array for each post you have in the posts array:
function processComment(post) {
post.bigCommentsObject = _.mapKeys(post.comments, '_id');
// now the comments array is no longer needed
return _.omit(post, ['comments']);
}
Now use that function to turn each comments array into a big object with all the comments WHILE it still is in the array. Then afterwards turn the array itself in a big object:
const commentsProcessed = _.map(action.payload.data, procesComment);
const postsState = _.mapKeys(commentsProcessed, '_id');
I believe nowadays JS builtin function can do this without requiring external libraries. Anyway this should be the way to go. I will really encourage you getting back to js builtin functions.
var data = [
{
_id: '3',
title: 'Going on vaccation',
comments:[
{_id: 1, comment: 'hello'},
{_id: 2, comment: 'world'}
]
},
{
_id: '2',
title: 'Going to dinner',
comments:[
{_id: 1, comment: 'hello'},
{_id: 2, comment: 'world'}
]
}
]
//you can use JS builtin reduce for this
var transformedPost= _.reduce(data, function(posts, post) {
var newPost = Object.assign({}, post)
newPost._id=post._id
//you can use js builtin map for this
newPost.comments = _.mapKeys(post.comments, '_id')
// if you are using es6, replace the last three line with this
//return Object.assign({}, posts, {[newPost._id]: newPost})
var item = {}
item[newPost._id]=newPost
return Object.assign({}, posts, item)
},{});
console.log(transformedPost)
https://jsbin.com/suzifudiya/edit?js,console

Categories