I have a component that fetches data asynchronously in componentDidMount()
componentDidMount() {
const self = this;
const url = "/some/path";
const data = {}
const config = {
headers: { "Content-Type": "application/json", "Accept": "application/json" }
};
axios.get(url, data, config)
.then(function(response) {
// Do success stuff
self.setState({ .... });
})
.catch(function(error) {
// Do failure stuff
self.setState({ .... });
})
;
}
My test for the component looks like this -
it("renders the component correctly", async () => {
// Have API return some random data;
let data = { some: { random: ["data to be returned"] } };
axios.get.mockResolvedValue({ data: data });
const rendered = render(<MyComponent />);
// Not sure what I should be awaiting here
await ???
// Test that certain elements render
const toggleContainer = rendered.getByTestId("some-test-id");
expect(toggleContainer).not.toBeNull();
});
Since rendering and loading data is async, my expect() statements go ahead and execute before componentDidMount() and the fake async call finish executing, so the expect() statements always fail.
I guess I could introduce some sort of delay, but that feels wrong and of course increases my runtime of my tests.
This similar question and this gist snippet both show how I can test this with Enzyme. Essentially they rely on async/await to call componentDidMount() manually.
However react-testing-library doesn't seem to allow direct access to the component to call its methods directly (probably by design). So I'm not sure "what" to wait on, or whether that's even the right approach.
Thanks!
It depends on what your component is doing. Imagine your component shows a loading message and then a welcome message. You would wait for the welcome message to appear:
const { getByText, findByText } = render(<MyComponent />)
expect(getByText('Loading...')).toBeInTheDocument()
expect(await findByText('Welcome back!')).toBeInTheDocument()
The best way to think about it is to open the browser to look at your component. When do you know that it is loaded? Try to reproduce that in your test.
You need to wrap render with act to solve warning message causes React state updates should be wrapped into act.
e.g:
it("renders the component correctly", async () => {
// Have API return some random data;
let data = { some: { random: ["data to be returned"] } };
axios.get.mockResolvedValue({ data: data });
const rendered = await act(() => render(<MyComponent />));
// Test that certain elements render
const toggleContainer = rendered.getByTestId("some-test-id");
expect(toggleContainer).not.toBeNull();
});
Also same goes for react-testing-library.
Related
I have an API called getQuote and a component called QuoteCard. Inside QuoteCard I'm trying to render an array of users that liked a quote. The API works fine, I have tested it, and the code below for getting the users works fine too.
const Post = async (url, body) => {
let res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"accept": "*/*"
},
body: JSON.stringify(body)
}).then(r => r.json());
return res;
}
const getAllLikes = async () => {
let users = await Post('api/getQuote', {
id: "639e3aff914d4c4f65418a1b"
})
return users
}
console.log(getAllLikes())
The result is working as expected :
However, when trying to map this promise result array to render it onto the page is where I have problems. I try to render like this:
<div>
{getAllLikes().map((user) => (
<p>{user}</p>
))}
</div>
However, I get an error that states:
getAllLikes(...).map is not a function
I don't understand why this is happening. Why can't I map the array? Is it because it's a promise or something?
And if anyone needs to see the getQuote API, here it is:
//Look ma I wrote an API by myself! :D
import clientPromise from "../../lib/mongodb";
const ObjectId = require('mongodb').ObjectId;
import nc from "next-connect";
const app = nc()
app.post(async function getQuote(req, res) {
const client = await clientPromise;
const db = client.db("the-quotes-place");
try {
let quote = await db.collection('quotes').findOne({
_id: new ObjectId(req.body.id)
})
res.status(200).json(JSON.parse(JSON.stringify(quote.likes.by)));
} catch (e) {
res.status(500).json({
message: "Error getting quote",
success: false
})
console.error(e);
}
})
export default app
Thanks for any help!
It is due to the fact that getAllLikes is an async function and thus it returns promise which does not have a map function.
You can either save it in a state variable before using await Or chain it with .then.
Minimal reproducible example which works
const getAllLikes = async () => {
return ['a', 'b']
}
getAllLikes().then((r) => r.map((g) => { console.log(g) }))
Edit: The above code won't work if directly used with jsx since the return of getAllLikes will still be a promise. Solution would be to save it in a state variable and then using it.
I am from Angular and I believe we call pipe on Observables (or Promises). Map can then be called inside the pipe function
observable$ = getAllLikes().pipe(map( user => <p>{user}</p>))
If there is no pipe, I can only think of manually subscribing (which is not a good practice)
sub$ = getAllLikes().subscribe( user => <p>{user}</p>)
// unsub from sub$ appropriately
// We do this from ngOnDestroy in angular
ngOnDestroy() {
this.sub$?.unsubscribe()
}
There is a requirement of cancelling the request calls when navigating away from the page or when the same api call is made multiple calls ( keeping the last one active).
This is how the API is extracted out( just a high level)
AJAX.ts
export async function customAjax(options){
let options = {};
options.headers = { ...options.headers, ...obj.headers };
const response = await fetch(url, options);
await response.json()
}
GET and POST calls are being extracted as
API.ts
const get = (url, extra = {}) => request({ url, type: "GET", ...extra });
const post = (url, payload, extra = {}) => request({ url, data: payload ,type: "POST",
}, ...extra });
In the react component I call these utilities as follows:
function MyComponent(){
useEffect(() => {
makeCall();
}, []);
async function makeCall(){
const { response, error } = await API.post(URL, payload);
// Handling code is not added here
// In the similar fashion GET calls are also made
}
}
I have come across Abortcontroller to cancel request where we could use abort method during unmounting of the component.
Is there a way to do this at a utililty level, may be inside customAjax so that I could avoid writing abort controller code everywhere?
From my understanding... What you describe is no different than a memory leak issue. And the current method for avoiding memory leaks is with the AbortController().
As far as handling this at the "utility level", I don't think this is feasible, and indeed would go against the preferred notion of an api being unaware of what's going on at the React component level; i.e separation of concerns..
So, in order to accomplish your requirement, you'll need to use AbortController(), or a custom implementation using a boolean flag that reflects whether the component is mounted, on a per component basis.
Using the boolean flag, you may be able to accept an argument in your api, passing the flag as a parameter; but again, I think this would be considered an anti-pattern.
I understand you're looking for a minimal implementation; but standard practice is fairly minimal:
useEffect(() => {
let abortController = new AbortController();
// Async code
return () => { abortController.abort(); }
}, []);
Using a boolean flag would be more verbose, and would entail something like this in your case:
useEffect(() => {
let isMounted = true;
customAjax(isMounted);
return () => {
isMounted = false;
}
}, []);
To handle out-of-order ajax responses, you can use a local variable inside the effect. For example,
useEffect(() => {
let ignore = false;
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
if (!ignore) setProduct(json);
}
fetchProduct();
return () => { ignore = true };
}, [productId]);
The ignore variable will ensure that only the latest request's response is updated to state. Reference - https://reactjs.org/docs/hooks-faq.html#performance-optimizations
Regarding memory leak concerns, please see this discussion - https://github.com/reactwg/react-18/discussions/82
JavaScript----.I have an api response which should come before anything else loads up or renders in the browser but it gives response which is slower than the loading of first component which rendered on the page.
This the code for the api call which i make to get the data is given below.Is there any way i can make this given below to be executed synchronously.
const GetLanguageData = (pageName) => {
const traslationApiUrlObj = {
languageCode: $('#hdnLanguageCode').val(),
baseUrl: `${apiUrl}`,
page: pageName
}
const { baseUrl, languageCode, page } = traslationApiUrlObj;
const traslationApiUrl = `${baseUrl}${languageCode}/${page}`;
ResourceLanguageText = null;
fetch(traslationApiUrl)
.then(res => res.json())
.then((result) => {
ResourceLanguageText = result;
console.log('Result');
},
(error) => {
console.log(error);
});
}
it can be achieved using combination of state and componentdidmount() lifecycle method.
keep a boolean flag in state "loaded" initialized to true.
call api in componentdidmount(), after you receive response set "loaded" to true in state.
in your render() method, do a conditional render based on your loaded is true
I've made an application and want to add more components which will use the same json I fetched in "personlist.js", so I don't want to use fetch() in each one, I want to make a separate component that only does fetch, and call it in the other components followed by the mapping function in each of the components, how can make the fetch only component ?
here is my fetch method:
componentDidMount() {
fetch("data.json")
.then(res => res.json())
.then(
result => {
this.setState({
isLoaded: true,
items: result.results
});
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
error => {
this.setState({
isLoaded: true,
error
});
}
);
}
and here is a sandbox snippet
https://codesandbox.io/s/1437lxk433?fontsize=14&moduleview=1
I'm not seeing why this would need to be a component, vs. just a function that the other components use.
But if you want it to be a component that other components use, have them pass it the mapping function to use as a prop, and then use that in componentDidMount when you get the items back, and render the mapped items in render.
In a comment you've clarified:
I am trying to fetch the json once, & I'm not sure whats the best way to do it.
In that case, I wouldn't use a component. I'd put the call in a module and have the module expose the promise:
export default const dataPromise = fetch("data.json")
.then(res => {
if (!res.ok) {
throw new Error("HTTP status " + res.status);
}
return res.json();
});
Code using the promise would do so like this:
import dataPromise from "./the-module.js";
// ...
componentDidMount() {
dataPromise.then(
data => {
// ...use the data...
},
error => {
// ...set error state...
}
);
}
The data is fetched once, on module load, and then each component can use it. It's important that the modules treat the data as read-only. (You might want to have the module export a function that makes a defensive copy.)
Not sure if this is the answer you're looking for.
fetchDataFunc.js
export default () => fetch("data.json").then(res => res.json())
Component.js
import fetchDataFunc from './fetchDataFunc.'
class Component {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
Component2.js
import fetchDataFunc from './fetchDataFunc.'
class Component2 {
state = {
// Whatever that state is
}
componentDidMount() {
fetchFunc()
.then(res => setState({
// whatever state you want to set
})
.catch(err => // handle error)
}
}
You could also have a HOC that does fetches the data once and share it across different components.
Here's my child's component async method:
async created () {
this.$parent.$emit('loader', true)
await this.fetchData()
this.$parent.$emit('loader', false)
}
fetchData does an axios get call, to fetch data from API. However in a vue-devtools (events tab) i can only see the events, after i change the code and it hot reloads. Also i've set up console.log() in a parent component:
mounted() {
this.$on('loader', (value) => {
console.log(value)
})
}
And i can see only false in a console. My purpose is to emit loader set to true (so i can show the loader), then set it to false, when data is fetched.
My fetchData method:
import http from '#/http'
fetchData() {
return http.getOffers().then((resp) => {
this.offersData = resp.data
})
}
Contents of http.js:
import axios from 'axios'
import config from '#/config'
const HTTP = axios.create({
baseURL: config.API_URL
})
export default {
/* calculator */
getOffers() {
return HTTP.get('/url')
}
}
If i directly use return axios.get() in async created(), then it works. Problem is in this imported http instance.
Final solution
One of the problems was using different lifecycles, thanks to Evan for mentioning this.
Another problem was with async / await usage, changes to a fetchData() method:
import http from '#/http'
async fetchData() {
await http.getOffers().then((resp) => {
this.offersData = resp.data
})
}
I had to make this method async and use await on axios request, since await is thenable, it does work. Also i've spotted an issue in https.js:
export default {
/* calculator */
getOffers() {
return HTTP.get('/url')
}
}
It returns HTTP.get(), not a promise itself, i could have used then here, and it would work, but, for flexibility purposes i didn't do that.
But, still, i don't get why it didn't work:
fetchData() {
return http.getOffers().then((resp) => {
this.offersData = resp.data
})
}
Isn't it already returning a promise, since it's chained with then... So confusing.
Retested again, seems like return is working, lol.
The issue here is that created on the child component is getting called before mounted on the parent component, so you're beginning to listen after you've already started your Axios call.
The created lifecycle event method does not do anything with a returned promise, so your method returns right after you begin the Axios call and the rest of the vue component lifecycle continues.
You should be able to change your parent observation to the created event to make this work:
created() {
this.$on('loader', (value) => {
console.log(value)
})
}
If for some reason you need to do something that can't be accessed in created, such as accessing $el, I'd suggest moving both to the mounted lifecycle hook.
I'd simply suggest restructuring your method, as there isn't really a need to make an async method since axios itself is asnychronus.
If you already have the fetchData method defined, and the goal is to toggle the loader state when a call is being made, something like this should do.
fetchData () {
this.$parent.$emit("loader", true)
axios.get(url)
.then(resp => {
this.data = resp
this.$parent.$emit("loader", false)
})
}
Of course these then statements could be combined into one, but it's the same idea.
Edit: (using the parent emit function)
fetchData () {
this.loader = true
axios.get(url)
.then(resp => this.data = resp)
.then(() => this.loader = false)
}
If what you are trying to achieve is to tell the direct parent that it's no longer loading, you would have to emit to the same instance like so
async created () {
this.$emit('loader', true)
await this.fetchData()
this.$emit('loader', false)
}
By removing the$parent, you will emit from the current component.
--Root
--My-page.vue
-Some-child.vue
Now you will emit from some-child.vue to my-page.vue. I have not tried, but theoretically what you are doing by emiting via parent: (this.$parent.$emit('loader', false)) You are emitting from my-page.vue to root.
So If you have a $on or #loader on the component like so: <Some-child #loader="doSomething"/>, This will never run due to you emitting from the parent.