Check Store before API call NgRx Angular - javascript

I am creating NgRx application but I am quite confused about its implementation as it is my first app with NgRx.
I have a store with "Companies" state. I gotta search the companies and return if found.
If the required company is not found it should call the API and fetch the results likewise but the process is circular and runs infinite time.
Here is my code:
this.companySearchCtrl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(val => {
if (val !== ' ' || val !== '') {
this.store.select(getCompanys).subscribe(data => {
console.log(data);
//filter companies on the basis of search text
const filteredData = data.filter(x =>
x['name']
.toLowerCase()
.startsWith(this.companySearchCtrl.value.toLowerCase())
);
console.log(filteredData);
if (filteredData.length === 0) { //if data is not found in store
console.log('got a call');
this.store.dispatch(
new CompanyActions.find({
where: { name: { regexp: `${val}/i` } } // call to API to search with regExp
})
);
} else {
// if required data found in store
console.log('got no call');
this.filteredCompanies$ = of(filteredData);
}
});
}
});
This process runs fine if data is found in store. If data is not found in store or I dont get any results from API call it runs infinitely.
How can I make this correct?

Make a few conventions:
state.Companies = null is initial state if no request to server has been sent yet
state.Companies = [] is state after first request was sent but no companies returned from server
use createSelector that filters your companies based on criteria you need
use withLatestFrom in your effects which will enable you to check store state within effects
Now, turn the logic the other way around:
when you look for companies, first fire an action that will trigger effects
in that effect, check if state.Companies is null or not
if its null > fire api request
if its not null > fire an action that will trigger selector for filtering
if data was not found even if state.Companies was not null that means either you need to refresh your Companies collection or the value doesn't exist on server

Create another action named dataNotFound. If you found data then set its state isFound to true and if data does not find, set its state isFound to false and always before sending call with regex check isFound that either data was found in previous call or not. If data was not found then don't send call again.
I've made a little bit change in your code to manage this. You just have to create an action dataNotFound now.
this.companySearchCtrl.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe(val => {
if (val !== ' ' || val !== '') {
this.store.select(getCompanys).subscribe(data => {
console.log(data);
//filter companies on the basis of search text
const filteredData = data.filter(x =>
x['name']
.toLowerCase()
.startsWith(this.companySearchCtrl.value.toLowerCase())
);
console.log(filteredData);
if (filteredData.length === 0) { //if data is not found in store
console.log('got a call');
this.store.select(isDataFound).subscribe(isFound => {
if(isFound) {
this.store.dispatch(
new CompanyActions.find({
where: { name: { regexp: `${val}/i` } } // call to API to
search with regExp
})
);
} else {
this.store.dispatch(new CompanyActions.dataNotFound({isFound: false}));
}
});
} else {
// if required data found in store
console.log('got no call');
this.store.dispatch(new CompanyActions.dataNotFound({isFound: true}));
this.filteredCompanies$ = of(filteredData);
}
});
}
});

Related

Async issue with State in React Native

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
})
}

How to edit request url in service worker?

I'm using cache first caching strategy for my pwa, for every GET request I first look if that request exists in cache, if it does I return it and update the cache.
The problem is that users can switch between multiple projects, so when they switch to another project,
the first time they open some url, they get the stuff from previous project if it exists in cache.
My solution is to try to add GET parametar ?project=projectId(project=2 for example) in the service worker, so each project would have its own version of the request saved in the cache.
I wanted to concatinate project id to the event.request.url, but I've read here that it is read only.
After doing that, hopefully I would have urls like this in cache:
Instead of: https://stackoverflow.com/questions
I would have: https://stackoverflow.com/questions?project=1
And: https://stackoverflow.com/questions?project=2
So I would get questions from the project I'm on, instead of just getting questions from previous project is /questions is saved in cache already.
Is there a way to edit request url in service worker?
My service worker code:
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.clone().url);
if (event.request.clone().method === 'POST') {
// update project id in service worker when it's changed
if(url.pathname.indexOf('/project/') != -1 ) {
// update user data on project switch
let splitUrl = url.pathname.split('/');
if (splitUrl[2] && !isNaN(splitUrl[2])) {
console.log( user );
setTimeout(function() {
fetchUserData();
console.log( user );
}, 1000);
}
}
// do other unrelated stuff to post requests
.....
} else { // HANDLE GET REQUESTS
// ideally,here I would be able to do something like this:
if(user.project_id !== 'undefined') {
event.request.url = event.request.url + '?project=' + user.project_id;
}
event.respondWith(async function () {
const cache = await caches.open('CACHE_NAME')
const cachedResponsePromise = await cache.match(event.request.clone())
const networkResponsePromise = fetch(event.request.clone())
if (event.request.clone().url.startsWith(self.location.origin)) {
event.waitUntil(async function () {
const networkResponse = await networkResponsePromise.catch(function(err) {
console.log( 'CACHE' );
// return caches.match(event.request);
return caches.match(event.request).then(function(result) {
// If no match, result will be undefined
if (result) {
return result;
} else {
return caches.open('static_cache')
.then((cache) => {
return caches.match('/offline.html');
});
}
});
});
await cache.put(event.request.clone(), networkResponse.clone())
}())
}
// news and single photos should be network first
if (url.pathname.indexOf("news") > -1 || url.pathname.indexOf("/photos/") > -1) {
return networkResponsePromise || cachedResponsePromise;
}
return cachedResponsePromise || networkResponsePromise;
}())
}
});
It's possible to use any URL as a cache key when reading/writing to the Cache Storage API. When writing to the cache via put(), for instance, you can pass in a string representing the URL you'd like to use as the first parameter:
// You're currently using:
await cache.put(event.request.clone(), networkResponse.clone())
// Instead, you could use:
await cache.put(event.request.url + '?project=' + someProjectId, networkResponse.clone())
But I think a better approach that would accomplish what you're after is to use different cache names for each project, and then within each of those differently-named caches you would not have to worry about modifying the cache keys to avoid collisions.
// You're currently using:
const cache = await caches.open('CACHE_NAME')
// Instead, you could use:
const cache = await caches.open('CACHE_NAME' + someProjectId)
(I'm assuming that you have some reliable way of figuring out what the correct someProjectId value should be inside of the service worker, based on which client is making the incoming request.)

How to Iterate over the whole array? and then update the database

I am working on an event-related project where I need to make sure that two events can't be set in the same timings for the same venue.
For this, whenever a new event is added, I use find() to get all the events having same event venue and then iterate to check whether the new event timing slots are clashing with other events in the database with the same venue.
I am getting two errors:
1) can't set headers after they are sent
2) I think my logic of iteration is not correct, I want the array to be checked completely and then insert if the same event venue has different timings for any XYZ event.
I tried using the filter, reduce etc but can't seem to have the desired results.
_this.insert = function(req, res) {
var obj = new _this.model(req.body);
obj.save(function(err, item) {
if (err && err.code === 11000) {
res.sendStatus(400);
}
if (err) {
return console.error(err);
}
_this.model.find({ event_ground: req.body.event_ground }, function(
err,
docs
) {
events = docs;
events.map(event => {
if (event.event_date_time && event.event_end_time) {
endTimeofEvent = moment(event.event_end_time);
timeofEvent = moment(event.event_date_time);
let isStartTime = moment(req.body.event_date_time).isBetween(
timeofEvent,
endTimeofEvent
);
debugger
let isEndTime = moment(req.body.event_end_time).isBetween(
timeofEvent,
endTimeofEvent
);
debugger
if (isStartTime === false && isEndTime === false) {
console.log('DB UPDATED');
debugger
_this.userModel.updateOne(
{
_id: req.params.id
},
{
$push: {
events: item._id
}
},
function(err) {
if (err) {
res.status(404).json('Something Went Wrong');
}
res.sendStatus(200);
}
);
debugger
}
if(isStartTime === true || isEndTime === true) {
console.log('DB WONT BE UPDATED');
res.status(400).json({
success: false,
msg:
'This Venue is booked from ' +
timeofEvent.format('LLL') +
' & ' +
endTimeofEvent.format('LLL'),
status: false
});
}
}
});
});
});
};
If the events with the same name do not have time clash, the new event gets added, else if says event can be added, etc.
1) can't set headers after they are sent
function(err) {
if (err) {
res.status(404).json('Something Went Wrong');
}
res.sendStatus(200);
}
res.json() does not end the script. Always use return res.json() or add else condition if you don't want the code below executes.
2) I think my logic of iteration is not correct, I want the array to be checked completely and then insert if the same event venue has different timings for any XYZ event.
You are running map to iterate all the result but instantly update database once the condition matched (isStartTime === false && isEndTime === false).
It's fine if you have exactly one record but fail if the duplicate record is not the first result. I think it is better for you to do the condition check when searching database. If you have good reason to not filter when searching database, I suggest you to do as below:
let duplicate = events.filter(event => {
// filter records that match (isStartTime === true || isEndTime === true)
});
if (duplicate.length > 0) {
// return error message
} else {
// update database
}
The reason for
can't set headers after they are sent
Response for the request is already sent, try sending the response out of the map function. Also, add a return statement after sending a response if there are lines of code that can be executed afterward.
You don't need to iterate through all the docs in the find query. Use $gte and $lte operator to find whether there any event in the time.
your query can be modified to
_this.model.find({ event_ground: req.body.event_ground,event_date_time : {"$gte":req.body.event_date_time},event_end_time : {"$gte" :req.body.event_date_time } },

Calling then function after fetching data from firebase in ionic 3

I want fetch data from firebase after that I want to execute another function. Second function have to wait until first one is complete .
this.oAngularFireDatabase.database.ref('Users').orderByKey()
.on('value', snapshot => {
if (snapshot.hasChildren()) {
snapshot.forEach(innerSnap => {
if (innerSnap.hasChild(user.uid)) {
//User role key
this.loggedInUserUserRoleKey = innerSnap.key;
//User id
this.loggedInUserId = user.uid;
//User name
this.loggedInUserName = innerSnap.child(user.uid).child("user_name").val();
if (innerSnap.child(user.uid).hasChild("user_image")) {
//User Image
this.loggedInUserImage = innerSnap.child(user.uid).child("user_image").val();
}
return false;
}
})
}
})
I can't call then function after on it gives me an error.
In my above code, I want call another function after all data are fetch from firebase.
The Firebase on() method can fire multiple times: once when it initially loads the data, and again whenever the data changes. Since a promise (the thing you call then() on) can only resolve once, on() can't return a promise.
There are two options here:
You want to only load the data once.
If this is the case, you should use Firebase's once() method, which does return a promise.
this.oAngularFireDatabase.database.ref('Users').orderByKey()
.once('value').then(snapshot => {
if (snapshot.hasChildren()) {
snapshot.forEach(innerSnap => {
if (innerSnap.hasChild(user.uid)) {
//User role key
this.loggedInUserUserRoleKey = innerSnap.key;
//User id
this.loggedInUserId = user.uid;
//User name
this.loggedInUserName = innerSnap.child(user.uid).child("user_name").val();
if (innerSnap.child(user.uid).hasChild("user_image")) {
//User Image
this.loggedInUserImage = innerSnap.child(user.uid).child("user_image").val();
}
return false;
}
})
}
}).then(value => {
// TODO: perform subsequent action on boolean value
})
You want to listen for changes on the data too.
If this is the case, you should put the subsequent action you want to take into the on() callback:
this.oAngularFireDatabase.database.ref('Users').orderByKey()
.on('value', snapshot => {
if (snapshot.hasChildren()) {
snapshot.forEach(innerSnap => {
if (innerSnap.hasChild(user.uid)) {
//User role key
this.loggedInUserUserRoleKey = innerSnap.key;
//User id
this.loggedInUserId = user.uid;
//User name
this.loggedInUserName = innerSnap.child(user.uid).child("user_name").val();
if (innerSnap.child(user.uid).hasChild("user_image")) {
//User Image
this.loggedInUserImage = innerSnap.child(user.uid).child("user_image").val();
}
}
})
// TODO: perform subsequent action on data
}
})
Note that both of these operations look pretty expensive for what they're trying to accomplish: scanning a JSON tree for a specific value is an anti-pattern in Firebase, and typically means you should modify/augment your JSON to allow a direct lookup or query.
For example, I suspect you now have a structure like /Users/$randomkey/$uid: { ..user data... }. For better performance, consider storing the user data directly under their UID: /Users/$uid: { ..user data... }. This removes the need for a query, and allows you to directly load the data for a user from this.oAngularFireDatabase.database.ref('Users').child(user.uid).

Angular 2 callback

I created a service that gets the some data from the api this is the code
getChallenges(): Observable<IChallenge[]> {
if (this._challengeUrl != null) {
return this.http.get(this._challengeUrl)
.map((res:Response) => <IChallenge[]>res.json())
.do(data => console.log('data: ' + JSON.stringify(data)))
.catch(this.handleError);
} else {
//;
}
}
and i subscribe inside the component where i want to use the service inside ngOnInit and everything is running my fine.
this._challengeService.getChallenges()
.subscribe(challenges => this.challenges = challenges,
error => this.errorMessage = <any>error);
but now i need to use a filter on the data which should run after ngInit finishes getting the data. this is the filter:
filterByLvl(lvl){
this.challenges.filter((obj)=> obj.level == lvl);
}
well my problem is when i try to put the function after the subscribe code i keep getting an empty array because the ngOnInit runs this function first and then gets the data. how can i inverse this? i need to get the data and then run this function.
so any ideas on how to do this? and thanks
I haven't tried ( don't have access to angular2 at work :-( ), but you can have multiple statements in the lambda function in subscribe.
this._challengeService.getChallenges()
.subscribe(challenges =>
{
this.challenges = challenges;
filterByLvl(expert_level);
},
error => this.errorMessage = <any>error
);
One method would be filter directly when it retrieves the data something like:
this._challengeService.getChallenges()
.subscribe(challenges => this.challenges = challenges.filter((obj)=> obj.level == lvl),
error => this.errorMessage = <any>error);
NOTE The lvl will be undefined so you've to define it someway with your logic

Categories