Why does data from .subscribe() first return an empty object before filling the object with data? - javascript

I'm working with Observables to get data from the backend. In my function I'm subscribing to my observable and when I console.log() the data that is passed back it appears to return twice. First an empty object and then later the data I'm expecting.
This is causing a problem for me because I'm trying to use a for...in loop to compare the keys of the data with the keys of another object so I can match the values. I get a TypeError: Cannot read property '0' of undefined
because the data first returns an empty object. This is confusing to me because I'm doing console.log() inside the subscribe's callback method.
Isn't the whole point of the callback method to wait until the data has arrived?
I've tried a callback function as well as putting the for...in directly inside the subscribe and neither work because the object returns empty first. What am I doing wrong?
this.memberService.memberDetails.subscribe(member => {
this.member = member;
this.member_id = this.authService.loggedInUser.app_user_data.member_id;
this.parseAddress();
console.log('member subscribe', this.member);
this.formData();
});
// UPDATE: MemberService code
private _memberDetails = new BehaviorSubject<any>({});
public get memberDetails() {
return this._memberDetails.asObservable();
}
// Notice the console.log() has fired twice
formData() {
for (const flatProfileKey in this.profileData['permanent_address'][0]) {
for (const key in this.member['addresses'][0]) {
if (flatProfileKey === key) {
this.profileData[key] = this.flatProfile[key];
console.log('profileData perament_address', this.profileData['permanent_address'][0])
}
}
}
}
// If I try to loop through the data it returns an undefined error presumably because the subscribe first returns an empty object so there is nothing to loop through

This is expected as you are setting initial value to BehaviorSubject to empty object here:
private _memberDetails = new BehaviorSubject<any>({});
If you don't need an initial value you can consider using Subject instead of BehaviorSubject.
Read more about BehaviorSubject here: https://www.learnrxjs.io/subjects/behaviorsubject.html

This is working as expected.
Behavior subject will re-emit the last emitted value or the default value if no previous value has been emitted.
Now, see that you are providing the default value to the BehaviorSubject as an empty object. Since you are passing a default value to it, it will emit this default value to all the subscribers.
And when you retrieve the data and change the value of the BehaviorSubject then again it will emit the new data to the subscribers.
Also, if you are thinking to pass nothing as a default value to BehaviourSubject, you cannot do that.
Here, you have two options you can do:
You can add an additional if to check if the value emitted is default value {} or not. And according to that you handle the stuff. Here's the code:
formData() {
if(this.profileData['permanent_address']) {
// Data fetched, do something
for (const flatProfileKey in this.profileData['permanent_address'][0]) {
}
}
else {
// If data is not fetched, do something if you want to
}
}
You can use Subject instead of BehaviorSubject that doesn't need a default value. Since it doesn't have a value, it will not emit it before fetching the data. Just change below line:
private _memberDetails = new BehaviorSubject<any>({})
to
private _memberDetails = new Subject<any>()
And this will work as you expect it to work.
Hope this helps.

Related

How get initial value from an Observable Angular 8+

I have a page form with values already setted, previous and next button elements.
At ngOnInit, I'm getting a List with 3 items from an observable, as initial value - sometimes I get 4 items.
Before I go to the next page I have to click, necessarily, on a button that will call the function calculate() that will make a request and my observable List will have 4 items.
So, when I click on next button onNextButton() I would like to use the initial value to compare with the current, if they are the same, or check if this list had any changes (any incrementing).
The way that I'm doing, I'm not manage to keep/store the first value. On next button click, i'm getting the updated value, instead the previous.
My code:
export class PageComponent implements OnInit {
questions$: Observable<any[]>;
hasChange: boolean;
ngOnInit() {
// GETTING INITIAL VALUE
this.questions$.subscribe((quest: any[]) => {
this.hasChange = quest.length > 3 ? true : false
});
}
calculate() {
// calling a request
// at this point, my observable will update and will have 4 items.
}
onNextButton() {
if (hasChange) {
// submit()
} else {
// navigate()
}
}
}
So, in this scenario the initial value should be a list w/ 3 items, but i'm getting 4 and its breaking my logic...
How do I get the previous value of Observable and store it in a variable?
Or, how can I detect any changes?
I tried behavioursubject and pairwise from RxJS, but I'm not sure how to apply it.
Thank you!!!
As you stated, you must use ReplaySubject or BehaviourSubject, since Observable does not have that "memory" for retrieve last emitted value.
ReplaySubject and BehaviourSubject are similar, both send last emitted value on subscription, but ReplaySubject only emits last emitted value on subscription and BehaviourSubject stores a list of last emitted values (configurable size). I'll be using BehavoiurSubject since is a bit more complete.
Just replace the Observable you were using with the BehaviourSubject, and keep in mind that you must definde that memory size when you instance the class. For example, you could create a method that returns a BehaviourSubject with last boolean stored like this:
private fooBehaviourSubject;
function ngOnInit: void{
this.fooBehaviourSubject = new BehaviorSubject<boolean>(1); // 1 is the "stack" size
this.fooBehaviourSubject.next(true); // <- First value emitted here
}
function getValue: BehaviourSubject<boolean> {
return this.fooBehaviourSubject;
}
When you subscribe like this:
getValue().subscribe(e => console.log(e))
the last stored value (true) automatically will be retrieved and shown in console, but ensure you at least have emitted one first, or you wont execute the subscription until one next method is called. After that, every update of the value will trigger that console.log with the updated value.
Applied to your code, you could create the BehaviourSubject in the ngOnInit, subscribe to it also in the ngOnInit to define the callback event, and call the next method of the BehaviourSubject once the list must be updated.
export class PageComponent implements OnInit {
questions$: Observable<any[]>
hasChange: boolean
ngOnInit() {
// GETTING INITIAL VALUE
let hasSome = false
this.questions$
.subscribe((quest: any[]) => {
// ### This is works for me!
hasSome = quest.some(item => item.id === 'whatever')
// ### This is the way that I was trying to do.
// ### Is's not wrong but didn't work for me =(
// this.hasChange = quest.some(item => item.id === 'whatever')
)
this.hasChange = hasSome
}
calculate() {
// calling a request
// at this point, my observable will update and will have 4 items.
}
onNextButton() {
// ### Now this.hasChange is assigned once and where I want, OnInit.
if (this.hasChange) {
// submit()
} else {
// navigate()
}
}
}

Why does this hook call return undefined when I simply change the variable name?

I'm running an API call via redux - in the tutorial, I am following, they use the variable name "sales" to store the data. Following along, I kept getting undefined, and after some troubleshooting, it appears that the only way for me to get any data out of this API call is to save the result in a variable named exactly "data".
// Correctly retrieves and logs the data
const { data, isFetching } = useGetSalesQuery();
console.log(data);
// Returns "undefined" every time
const { anythingElse, isFetching } = useGetSalesQuery();
console.log(anythingElse);
data is not defined anywhere else within this component. So what's going on here? Was Redux updated to force us to always use the name "data"? This is doing my head in.
useGetSalesQuery returns an object that has data and isFetching. Attempting to access an arbitrary field from that object will get you undefined. What's going on in this component is that you are defining a variable data and assign it the value from the field data that is returned from useGetSalesQuery
See javascript's destructuring assignment

Angular subscribes not working how I expect

I'm at a loose end here and trying to understand the flow of how angular subscriptions work.
I make a call to an API and in the response I set the data in a behaviourSubject. So I can then subscribe to that data in my application.
Normally I would use async pipes in my templates cause its cleaner and it gets rid of all the subscription data for me.
All methods are apart of the same class method.
my first try.....
exportedData: BehaviourSubject = new BehaviourSubject([]);
exportApiCall(id) {
this.loadingSubject.next(true)
this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught)),
finalize(() => => this.loadingSubject.next(false))
).subscribe(res => {
this.exportedData.next(res)
})
}
export(collection) {
let x = []
this.exportCollection(collection.id); /// calls api
this.exportedData.subscribe(exportData => {
if(exportData){
x = exportData
}
})
}
console.log(x)//// first time it's empthy, then it's populated with the last click of data
/// in the template
<button (click)="export(data)">Export</button>
My problem is....
There is a list of buttons with different ID's. Each ID goes to the API and gives back certain Data. When I click, the console log firstly gives a blank array. Then there after I get the previous(the one I originally clicked) set of data.
I'm obviously not understanding subscriptions, pipes and behavior Subjects correctly. I understand Im getting a blank array because I'm setting the behaviour subject as a blank array.
my other try
export(collection) {
let x = []
this.exportCollection(collection.id).pip(tap(res => x = res)).subscribe()
console.log(x) //// get blank array
}
exportApiCall(id) {
return this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught))
)
}
Not sure about the first example - the placement of console.log() and what does the method (that is assigned on button click) do - but for the second example, you're getting an empty array because your observable has a delay and TypeScript doesn't wait for its execution to be completed.
You will most likely see that you will always receive your previous result in your console.log() (after updating response from API).
To get the initial results, you can update to such:
public exportReport(collection): void {
this.exportCollection(collection.id).pipe(take(1)).subscribe(res => {
const x: any = res;
console.log(x);
});
}
This will print your current iteration/values. You also forgot to end listening for subscription (either by unsubscribing or performing operators such as take()). Without ending listening, you might get unexpected results later on or the application could be heavily loaded.
Make sure the following step.
better to add console.log inside your functions and check whether values are coming or not.
Open your chrome browser network tab and see service endpoint is get hitting or not.
check any response coming from endpoints.
if it is still not identifiable then use below one to check whether you are getting a response or not
public exportReport(collection): void {
this.http.get(url+"/"+collection.id).subscribe(res=> {console.log(res)});
}
You would use BehaviourSubject, if there needs to be an initial/default value. If not, you can replace it by a Subject. This is why the initial value is empty array as BehaviourSubject gets called once by default. But if you use subject, it wont get called before the api call and you wont get the initial empty array.
exportedData: BehaviourSubject = new BehaviourSubject([]);
Also, you might not need to subscribe here, instead directly return it and by doing so you could avoid using the above subject.
exportApiCall(id) {
this.loadingSubject.next(true);
return this.api.getReport(id).pipe(
catchError((err, caught) => this.errorHandler.errorHandler(err, caught)),
finalize(() => => this.loadingSubject.next(false))
);
}
Console.log(x) needs to be inside the subscription, as subscribe is asynchronous and we dont knw when it might get complete. And since you need this data, you might want to declare in global score.
export(collection) {
// call api
this.exportApiCall(collection.id).subscribe(exportData => {
if (exportData) {
this.x = exportData; // or maybe this.x.push(exportData) ?
console.log(this.x);
}
});
}

Can't access array after passing it to state, but can access it before

I have an pseudo-object is inside my state. I've been able to access through a couple layers, but when I reach the array inside the object Im getting undefined errors.
UPDATE: Its something wrong with how I pass lambdaReturnObject to the state which isn't letting me access the array, tested with lambdaReturnObject.campaigns[0].campaignName and it worked.
handleSearch() {
//data to use to query backend
let campaignId = this.refs.campaignInput.value
let marketplace = this.refs.marketplaceInput.value
//using local copy of backend data, production should call backend fo this instead
let lambdaReturn = "{\"advertiser\":{\"advertiserId\":\"1\",\"enforcedBudget\":0.1},\"campaigns\":[{\"campaignID\":\"1\",\"campaignName\":\"fake\",\"createDate\":11111,\"creationDate\":1111,\"startDate\":1111,\"endDate\":1111,\"dailyBudget\":0.1,\"internal\":{\"budgetCurrencyCode\":\"USD\",\"inBudget\":true},\"enforcedBudget\":0.1,\"budgetCurrencyCode\":\"USD\",\"budgetPacingStrategy\":\"asp\",\"budgetType\":\"averageDaily\",\"status\":\"enables\",\"internalStatus\":\"enabled\"}],\"campaignID\":\"1\"}"
let lambdaReturnObject = JSON.parse(lambdaReturn)
this.setState({
apiData: lambdaReturnObject
})
}
When I try and go to the array inside, I get the following error
<h3>Campaigns :{console.log(this.state.apiData.campaigns[0].campaignName)}</h3>
Cannot read property '0' of undefined
This means I am accessing it the wrong way, but I looked at other posts (Accessing Object inside Array) and I thought that this was right. Though I am definitely wrong or else I wouldn't be writing this.
JSON.parse() is synchronous function, so set state wont be called till, JSON.parse() executes completely and returns the object.
Still You can try following
Call JSON.parse() using a try-catch block like below and see if it works. Also it is error free way of parsing your stringified objects.
try {
let lambdaReturnObject = JSON.parse(lambdaReturn)
this.setState({
apiData: lambdaReturnObject
})object
}
catch (err) {
// Do error handling here.
}
Use optional chaining, and try to access your object like this.state.apiData.campaigns?.[0].campaignName; this won't give error even if compaigns is undefined.
Refer : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
Depending on what is happening. The call to get campaigns[0] is getting resolved before the API call finishes. You can try a promise or async await to make sure the object is retrieved from the API call before you attempt to access it.
Do you happen to have a code snippet of is being used to get the data?
The error was that render() calls right as the page is booted up. At that time the object is not stored in my state so trying to call
this.state.objects.innerObject.property
would fail because I declared objects in my state, innerObject could be counted as null before the object is actually loaded in so it wouldn't throw an error. But property would throw an error because innerObject as we know, is null.
Fix by using an if statement before rendering the page to see if the object is actually loaded in. If not render just default empty JSX.

Attempt to render state value from ajax call in componentDidMount failing (React)

I've made an AJAX call in React with Axios, and I'm a bit confused about how the response is dealt with.
Here is my code:
componentDidMount() {
axios.get('https://jsonplaceholder.typicode.com/users')
.then( res => {
const users = res.data
this.setState({ users });
});
}
render() {
console.log(this.state.users)
return (
<div>
{this.state.users.map( user => <p>{user.name}</p>)}
</div>
)
}
When I console.log the results, two values are returned:
[] - the empty array assigned to the initial state
The expected array from the API.
This presumably can be explained by the state value being returned once on initialisation, and then again with componentDidMount.
My problem arises in how I access the this.state.users value. Obviously any time I access this, I want the values returned from the AJAX response, but if, for example I try to render the name property from the first object in the array...
{this.state.users[0].name}
...it attempts to access [], and thus returns nothing.
However, if I try to iterate through the arrays elements...
{users.map( user => <p>user.name</p> )}
...it returns the values as expected.
I don't know why it works with the second example but not the first. Surely if the first is attempting to pull data from the initial state value (an empty array), then when mapping through this.state.users it would also be attempting to map through an empty array and return nothing, or an error.
I'm obviously not fully understanding the React rendering process here and how componentDidMount works in the component lifecycle. Am I wrong in trying to assign the response value directly to state?
Would appreciate if anyone could help to clear this up.
When you use map() on this.state.users initially, it will do so over an empty array (which won't render anything). Once the response arrives, and this.setState({ users }) is called, render() will run again. This time, the array of users is no longer empty and the map() operation will be performed on each of the users in the array.
However, when you use {this.state.users[0].name} initially on the empty array, you're trying to access the name property of an object that doesn't yet exist — which will result in an error. To prevent the error, you could do a check before accessing the properties of the object:
{this.state.users.length > 0 && this.state.users[0].name}
Now it will behave the same way as in the first example. I.e. the first render() won't actually do anything, since this.state.users.length = 0. But as soon as this.setState() is called with new users, render() will run again, and this time this.state.users[0].name is available.
There is another way with constructor. just add your state with empty array users as-
constructor(props) {
super(props);
this.state = {
users: []
};
}
With this component first rendered with empty users array. When you trigger state update, component rerender with new array.

Categories

Resources