React js setstate not working in nested axios post - javascript

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned.
I just need to assign the value from 'res.data.id' to 'metricId' so that I can use it somewhere else in another function like save2() function.
Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);

This part of code run sync
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
but there you run async
axios.get(api1, getDefaultHeaders())
.then(() => {
You can refactor your code to async/await like this:
const save1 = async () => {
const response = await axios.get(api1, getDefaultHeaders());
const response2 = await axios.post(api2, { item1: response.data.item1 }, getDefaultHeaders());
return response2.data.id;
};
const save2 = (activeId) => {
console.log(activeId); // result: null
};
const handleSaveAll = async () => {
const activeId = await save1();
save2(activeId);
setActiveId(activeId);
console.log(activeId); // result: again its still null
};
or to chain of promises, like this:
const save2 = (activeId) => {
console.log(activeId); // result: null
};
const save1 = () => {
return axios.get(api1, getDefaultHeaders())
.then(({ data }) => {
const data = {item1: item1,};
return axios.post(api2, {item1: data.item1}, getDefaultHeaders())
})
.then((res) => res.data.id);
};
const handleSaveAll = () => {
save1()
.then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
return res.data.id;
})
.then(save2);
};

Related

Testing a void async method in Angular, the spec is not waiting for the async calls to complete, which is making the test always fail

I have a method called loadIssues() in an Angular component called BurndownComponent that is void and inserts values into an array. This method uses some async methods from a service and from the component itself. After running those asyncs it fills in the array.
I want to write test code for loadIssues(), but I do not know how to make the test wait for the array to be filled. Because loadIssues is void I cannot use async await. I also tried using a setTimeout but it ran asynchronously and did not wait for the execution of loadIssues().
Does anyone have an idea on how I could write such a test?
The relevant codes are below:
loadIssues():
loadIssues(): void {
this.selectedMilestone = this.milestone;
this.issues = [];
console.log(this.selectedMilestone.reviewDate);
this.gitlabService.getIssues(this.selectedMilestone?.title).then(issues => {
let allIssues = issues.filter(i => [RequirementLabel.StoryTask, RequirementLabel.Task, RequirementLabel.Story].includes(i.requirement));
this.getIssueEvents(allIssues).then(issues => {
allIssues = issues;
console.log('allIssues ', allIssues.length);
// issues could be moved out of the milestone towards the end of it
// we consider a limit of 3 days before the review meeting date
if (new Date().getTime() >= this.selectedMilestone.reviewDate.getTime() - (3 * MILLISECONDS_PER_DAY)) {
this.getMilestoneRolledIssues().then(rolledIssues => {
const issuesIds = allIssues.map(i => i.id);
console.log(issuesIds);
allIssues = allIssues.concat(...rolledIssues.filter(i => !issuesIds.includes(i.id))); // removes duplicated issues
this.gitlabService.getDiscussions(allIssues).then(discussions => {
allIssues.forEach((issue, index) => issue.discussions = discussions[index]);
this.issues = allIssues;
});
});
}
else {
this.gitlabService.getDiscussions(allIssues).then(discussions => {
allIssues.forEach((issue, index) => issue.discussions = discussions[index]);
this.issues = allIssues;
});
}
});
});
Test attempt (BurndownComponent.spec.ts):
describe('BurndownComponent', () => {
let component: BurndownComponent;
let fixture: ComponentFixture<BurndownComponent>;
const data: object = jsonData;
let httpMock: object;
let stubGitLabService: GitlabService;
beforeEach(async () => {
httpMock = {
'get': (url, headers): Observable<object[]> => {
const endpoint = 'https://git.tecgraf.puc-rio.br/api/v4/';
const discussions = data['discussions-test'][0]['discussions']
.map(d => Discussion.getDiscussion(d));
const urlDiscussions = [
`${endpoint}projects/1710/issues/120/discussions`,
`${endpoint}projects/1710/issues/97/discussions`,
`${endpoint}projects/1210/issues/920/discussions`
];
if(urlDiscussions.includes(url)) {
return new Observable(subscriber => discussions[urlDiscussions.indexOf(url)]);
}
return new Observable(subscriber => null);
}
}
stubGitLabService = new GitlabService(<any> httpMock);
await TestBed.configureTestingModule({
declarations: [ BurndownComponent ],
providers: [
{ provide: GitlabService, useValue: stubGitLabService }
]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BurndownComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('loads all issues - loadIssues()', async () => {
const milestoneData = Milestone.getMilestone(data['milestone-test'][0]);
const milestoneEventsData = data['all-milestone-events'][0]['events']
.map(me => MilestoneEvent.getMilestoneEvent(me));
const labelEventsData = data['label-events-burndown-component-test'][0]['events']
.map(le => LabelEvent.getLabelEvent(le));
const issues = data['issues-test-passed-milestone']
.map(i => Issue.getIssue(i));
const discussions = data['discussions-test'][0]['discussions']
.map(d => Discussion.getDiscussion(d));
issues.forEach((issue, index) => {
issue.labelEvents = labelEventsData.map(le => LabelEvent.copy(le));
issue.milestoneEvents = milestoneEventsData.map(me => MilestoneEvent.copy(me));
});
component.milestone = milestoneData;
stubGitLabService['getDiscussions'] = (issues: Issue[]): Promise<Discussion[][]> => {
return new Promise<Discussion[][]>(resolve => resolve(discussions))
};
const spyMilestoneRolledIssues = spyOn(component, 'getMilestoneRolledIssues')
.and
.returnValue(Promise.resolve(issues));
const spyIssueEvents = spyOn(component, 'getIssueEvents')
.and
.returnValue(Promise.resolve(issues));
const getDiscussionsSpy = spyOn(stubGitLabService, 'getDiscussions')
.and
.returnValue(new Promise(
resolve => {
console.log('discussions');
resolve(discussions)
}
));
await component.loadIssues();
expect(component.issues.length).toBe(3);
expect(spyMilestoneRolledIssues).toHaveBeenCalled();
expect(getDiscussionsSpy).toHaveBeenCalled();
});
You are awaiting loadIssues but it is not an async function. You can address this by returning the Promise returned by then which will complete at an appropriate time.
loadIssues() { // remove explicit void return type
this.selectedMilestone = this.milestone;
this.issues = [];
console.log(this.selectedMilestone.reviewDate);
// return the promise
return this.gitlabService.getIssues(this.selectedMilestone?.title).then(issues => {
...

useState value is undefined inside async function

I have a custom hook that saves/loads from cacheStorage.
const useCache = (storageKey, dir) => {
const [cache, setCache] = useState()
useEffect(() => {
const openCache = async () => {
const c = await caches.open(storageKey)
setCache(c)
}
openCache()
},[])
const save = async (res) => {
console.log(cache)
//prints undefined
cache.put(dir, new Response(JSON.stringify(res)))
const load = async () => {
console.log('load', cache)
//prints undefined
const res = await cache.match(dir)
return await res.json()
}
return { save, load }
}
and this useCache hook is used inside of another custom hook useConfigs
const useConfigs = (key, defaultValue = false) => {
const { save, load } = useCache('configs', '/configs')
(...)
const getConfigs = async () => {
const res = await fetchFromNetwork()
save(res)
}
const getLocalConfigs = async () => {
const res = await load()
(...)
return res
}
The issue is that useState variable cache returned null when both save() and load() are called inside getConfigs() and getLocalConfigs(). Seems like the value is undefined because of closure. If that is the reason, what would be the solution to update the cache variable by the time save() and load() are called?

Cannot destructure property

TypeError: Cannot destructure property results of 'undefined' or
'null'.
at displayCartTotal
const displayCartTotal = ({results}) => {
};
const fetchBill = () => {
const apiHost = 'https://randomapi.com/api';
const apiKey = '006b08a801d82d0c9824dcfdfdfa3b3c';
const apiEndpoint = `${apiHost}/${apiKey}`;
fetch(apiEndpoint)
.then( response => {
return response.json();
})
.then(results => {
console.log(results.results)
displayCartTotal();
})
.catch(err => console.log(err));
};
You get the error because you aren't passing results into displayCartTotal like displayCartTotal(results)
You are calling displayCartTotal() with no parameter, but it expects an object. See commented line below:
const displayCartTotal = ({results}) => {
};
const fetchBill = () => {
const apiHost = 'https://randomapi.com/api';
const apiKey = '006b08a801d82d0c9824dcfdfdfa3b3c';
const apiEndpoint = `${apiHost}/${apiKey}`;
fetch(apiEndpoint)
.then( response => {
return response.json();
})
.then(results => {
console.log(results.results)
displayCartTotal(); //<--- this is the offending line
})
.catch(err => console.log(err));
};
You should pass results as a parameter like this: displayCartTotal(results).
Hope that solves it for you :)

Async/await in componentDidMount to load in correct order

I am having some troubles getting several functions loading in the correct order. From my code below, the first and second functions are to get the companyID companyReference and are not reliant on one and another.
The third function requires the state set by the first and second functions in order to perform the objective of getting the companyName.
async componentDidMount() {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
a;
b;
c;
}
componentWillUnmount() {
this.isCancelled = true;
}
companyIdParams = () => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
});
};
getCompanyReference = () => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
});
});
};
getCompanyName = () => {
const { firebase } = this.props;
const { companyID, companyReference } = this.state;
const cid = companyID;
if (companyReference.includes(cid)) {
const getCompany = firebase.company(cid);
getCompany.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyName: doc.data().companyName,
loading: false
});
});
} else if (cid !== null && !companyReference.includes(cid)) {
navigate(ROUTES.INDEX);
}
};
How can I achieve this inside componentDidMount?
setState is asynchronous, so you can't determinate when the state is updated in a sync way.
1)
I recommend you don't use componentDidMount with async, because this method belongs to react lifecycle.
Instead you could do:
componentDidMount() {
this.fetchData();
}
fetchData = async () => {
const a = await this.companyIdParams();
const b = await this.getCompanyReference();
const c = await this.getCompanyName();
}
2)
The companyIdParams method doesn't have a return, so you are waiting for nothing.
If you need to wait I would return a promise when setState is finished;
companyIdParams = () => {
return new Promise(resolve => {
const urlString = location.href;
const company = urlString
.split('/')
.filter(Boolean)
.pop();
!this.isCancelled &&
this.setState({
companyID: company
}, () => { resolve() });
});
};
The same for getCompanyReference:
getCompanyReference = () => {
return new Promise(resolve => {
const { firebase, authUser } = this.props;
const uid = authUser.uid;
const getUser = firebase.user(uid);
getUser.onSnapshot(doc => {
!this.isCancelled &&
this.setState({
companyReference: doc.data().companyReference
}, () => { resolve() });
});
});
};
3)
If you want to parallelize the promises, you could change the previous code to this:
const [a, b] = await Promise.all([
await this.companyIdParams(),
await this.getCompanyReference()
]);
4)
According to your code, the third promise is not a promise, so you could update (again ;) the above code:
const [a, b] = .....
const c = this.getCompanyName()
EDIT: the bullet points aren't steps to follow
As the last api call is dependent on the response from the first 2 api calls, use a combination of Promise.all which when resolved will have the data to make the last dependent call
async componentDidMount() {
let [a, c] = await Promise.all([
this.companyIdParams(),
this.getCompanyReference()
]);
const c = await this.getCompanyName();
}

create function with property that can be accessed in and out of function scope

I have a function:
const fetchMovies = (function (query) {
const requestId = this.requestId(query)
return dispatch => {
dispatch(sendingRequest(requestId))
return ajax.get(`/movies/search?q=${query}`)
.then(res => {
dispatch(receievedResponse(requestId))
return dispatch(addMovies(res.data.movies))
})
}
}).bind({
requestId: (query) => `fetchMoviesLoading-${query}`
})
This allows the fetchMovies function to have the requestId able to be called within itself. However, the requestId property cannot be accessed like so:
fetchMovies.requestId === undefined // true
Is there a simple/clean way to expose the requestId?
This just looks messy:
const fetchMoviesContext = {
requestId: (query) => `fetchMoviesLoading-${query}`
}
const fetchMovies = (function (query) {
const requestId = this.requestId(query)
return dispatch => {
dispatch(sendingRequest(requestId))
return ajax.get(`/movies/search?q=${query}`)
.then(res => {
dispatch(receievedResponse(requestId))
return dispatch(addMovies(res.data.movies))
})
}
}).bind(fetchMoviesContext)
fetchMovies.requestId = fetchMoviesContext.requestId
Just use
function fetchMovies(query) {
const requestId = fetchMovies.requestId(query)
// ^^^^^^^^^^^
return dispatch => {
dispatch(sendingRequest(requestId))
return ajax.get(`/movies/search?q=${query}`)
.then(res => {
dispatch(receievedResponse(requestId))
return dispatch(addMovies(res.data.movies))
})
}
}
fetchMovies.requestId = (query) => `fetchMoviesLoading-${query}`;
Don't overcomplicate it.

Categories