Items being displayed when query is empty in react - javascript

I am learning react, and I have made a sample tv show app using an example off freecodecamp. It is all working okay from what I can see however after searching for something and then backspacing everything in the search box, it shows results when it shouldn't be, can anyone see a mistake I have made in my code?
class SeriesList extends React.Component {
state = {
series: [],
query: ''
}
onChange = async e => {
this.setState({query: e.target.value});
const response = await fetch(
`https://api.tvmaze.com/search/shows?q=${this.state.query}`
);
const data = await response.json();
this.setState({series: data});
}
render(){
return (
<div>
<input type="text" onChange={this.onChange} value={this.state.query} />
<ul>
<SeriesListItem list={this.state.series} />
</ul>
</div>
);
}
}
I have it on codepen here.
https://codepen.io/crash1989/pen/ERxPGO
thanks

One else point you can use await for setState
onChange = async e => {
await this.setState({query: e.target.value});
const response = await fetch(
`https://api.tvmaze.com/search/shows?q=${this.state.query}`
);
const data = await response.json();
this.setState({series: data});
}
Your request will be performed after changing query

setState works async. So there is no guarantee, that this.state.query has been updated after calling this.setState({query: e.target.value}). So your url contains the previous state eventually.
There are two options:
Use the event data for the new query:
const response = await fetch(
`https://api.tvmaze.com/search/shows?q=${e.target.value}`
);
...
Use the setState callback second arg
this.setState({query: e.target.value}, () => {
const response = await fetch(
`https://api.tvmaze.com/search/shows?q=${this.state.query}`
);
...
})
This is a good article about the async nature of setState https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3.

Problem
There are race conditions: When you type two+ letters you make two+ network calls. The first network call may be last to complete, and then you show results for the wrong query.
I fixed it in this pen: https://codepen.io/arnemahl/pen/zaYNxv
Solution
Keep track of results for all queries you have ever gotten the response to. Always how the data returned for the current query. (Also, don't make multiple API calls for the same query, we already know).
state: {
seriesPerQuery: {}, // Now a map with one result for each query you look up
query: [],
}
...
onChange = async e => {
...
// Now keep the results of the old querys, in addition to the new one
// whenever you get an API response
this.setState(state => ({
seriesPerQuery: {
...state.seriesPerQuery,
[query]: data,
}
}));
}

Related

Cannot render and map POST request array promise

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

How to use async await in React components without useEffect

My app has a search bar where the user types in a word and clicks search. Upon click, the app fetches the definition of that word from an open dictionary API, and then updates the parent component's state to reflect the results. The component is then supposed to render the results by passing that as a prop to a presentational child component.
However, it looks like the state gets set before the fetch call has the time to return the data. So the search results are not rendered until the search button is clicked again, causing confusion.
I have tried resolving this by making my function asynchronous (Dictionary.search refers to an imported function which handles the fetch and returns the result in the form of an array):
async search(term) {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
}
However this only works some of the time. For longer searches, it doesn't wait before updating the state and re-rendering.
I have done some googling and came across this suggestion, but it also doesn't work:
search = (term) => {
const dictionarySearch = async (term) => {
const results = await Dictionary.search(term);
this.setState({ searchTerm: term, results: results });
};
dictionarySearch(term);
};
EDITED TO ADD: Here is the Dictionary.search code, along with its helper function:
//Create an empty array in which the results of the API call will be stored
let results = [];
const Dictionary = {
search(term) {
//Url with our search term and API key:
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
//Fetch the results from Merriam Webster API:
fetch(url)
.then((response) => {
//Catch an error from their server
if (!response.ok) {
throw new Error("Network response was not ok");
}
//Return javaScript object version of the response
return response.json();
})
.then((jsonResponse) => {
//Perform the helper function on the javascript object and return the result (array)
return shortDef(jsonResponse);
})
//Catch any other errors than a server error
.catch((error) => {
console.error(
"There has been a problem with your fetch operation:",
error
);
});
//Create a copy of the results array
let returnResults = results.slice();
//Reset the original array to an empty array
results = [];
//Return the copy
return returnResults;
},
};
//Helper function to extract only certain info from the API
function shortDef(response) {
response.forEach((object) => {
//Create a new object for each object int he response array
let result = { word: "", type: "", definitions: [] };
//Add the word and type to the object
result.word = object.hwi.hw;
result.type = object.fl;
//Add the definitions to the object. There may be several, so it is pushed to an array.
let defs = object.shortdef;
defs.forEach((def) => {
result.definitions.push(def);
});
//Add the object to the array of API results
results.push(result);
});
//Return the list of results
return results;
}
I don't want to call the API in the ComponentDidMount, because it should get called every time the user presses "search". I also would prefer not to use useEffect, as it would mean refactoring my entire component from a class to a function.
Is there no way to have the setState in a class component wait for an asynchronous task to complete?
The problem is that your Dictionary.search function immediately returns, because it does not wait until the .then block resolves. Change it to an async function and await the fetch of the url. It should look like this:
const Dictionary = {
// Make search an async function
search: async term => {
const url = `https://www.dictionaryapi.com/api/v3/references/collegiate/json/${term}?key=${api_key}`;
// Await the results
await fetch(url)
.then(response => {
// ...
})
.then(jsonResponse => {
// ...
})
.catch(error => {
// ...
});
return;
},
};

Why does the UI not update for async/await function

This is my first website created with JavaScript. I'm trying to get items from API and then display them for the user.
My function:
getOrderItem = async (itemId: string) => {
console.log(await api.getItem(itemId))
return await api.getItem(itemId)
}
This is how I'm calling it in my UI:
<h4>Quantity: {item.quantity} Name: {this.getOrderItem(item.id).name}</h4>
My UI does not update but I can see that the function is working:
What I'm doing wrong with await/async?
try using the setState() hook with the useEffect() hook so you can tell React to re-render the DOM elements once your getOrderItem fulfills.
You can try setting it up like this:
const [getOrderItem, setOrderItem] = useState({})
getOrderItem = async (itemId: string) => {
console.log(await api.getItem(itemId))
return await api.getItem(itemId)
}
useEffect(() => {
setOrderItem(getOrderItem())
}, [])
return (
<h4>Quantity: {item.quantity} Name: {getOrderItem.name}</h4>
)
your function doesn't return anything until you get your response from the API. And by the time you get your response the UI is rendered already. Since the function doesn't update any state variables your UI doesn't re-render. You should use a state variable to store the response of the API.
getOrderItem = async (itemId: string) => {
let response = await api.getItem(itemId);
this.setState({items: {...this.state.items, itemId: response}});
}
Now, you can use the state variable to re-render
<h4>Quantity: {item.quantity} Name: {item.id in this.state.items ? this.state.items[item.id].name ? this.getOrderItem(item.id) }</h4>
If you only need the names of the items, you can store response.name instead of response

React: String automatically converted to [object promise] when called from another component

I'm developing the front-end for my spring boot application. I set up an initial call wrapped in a useEffect() React.js function:
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
'/myapi/' + auth.authState.id
);
setData(data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
The data returned isn't comprehensive, and needs further call to retrieve other piece of information, for example this initial call return an employee id, but if I want to retrieve his name and display it I need a sub-sequential call, and here I'm experiencing tons of issues:
const getEmployeeName = async id => {
try {
const name = await fetchContext.authAxios.get(
'/employeeName/' + id
);
console.log((name["data"])); // <= Correctly display the name
return name["data"]; // return an [Object promise],
} catch (err) {
console.log(err);
}
};
I tried to wrap the return call inside a Promise.resolve() function, but didn't solve the problem. Upon reading to similar questions here on stackoverflow, most of the answers suggested to create a callback function or use the await keyword (as I've done), but unfortunately didn't solve the issue. I admit that this may not be the most elegant way to do it, as I'm still learning JS/React I'm open to suggestions on how to improve the api calls.
var output = Object.values(data).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Async functions always return promises. Any code that needs to interact with the value needs to either call .then on the promise, or be in an async function and await the promise.
In your case, you should just need to move your code into the existing useEffect, and setState when you're done. I'm assuming that the employeeID is part of the data returned by the first fetch:
const [name, setName] = useState('');
useEffect(() => {
const getData = async () => {
try {
const { data } = await fetchContext.authAxios.get(
"/myapi/" + auth.authState.id
);
setData(data);
const name = await fetchContext.authAxios.get(
'/employeeName/' + data.employeeID
);
setName(name.data);
} catch (err) {
console.log(err);
}
};
getData();
}, [fetchContext]);
// ...
var output = Object.values(appointmentsData).map((index) =>
<Appointment
key={index["storeID"].toString()}
// other irrelevant props
employee={name}
approved={index["approved"]}
/>);
return output;
Note that the above code will do a rerender once it has the data (but no name), and another later when you have the name. If you want to wait until both fetches are complete, simply move the setData(data) down next to the setName

Cannot console.log fetch results

I'm trying to do an API call using fetch().
I'm aware that fetch() returns a Promise and should be handled using .then or await. The same for the result.json() Followed this tutorial http://www.reactnativeexpress.com/networking, I arrived with fetchRoute()function. The console.log(route) inside the function is never called.
I tried to return console.log(fetchRoute(this.state.userLocation, text)), but it was still returning a Promise.
I read another quesiton here on Stack Overflow (sorry, can't find the link anymore), and they said to try something like this:
getRouteHandler = (text) => {
fetchRoute(this.state.userLocation, text).then(json => console.log(json));
Still, I couldn't log the fetch results. Anyone knows what could be going wrong? Here is the relevant code:
const fetchRoute = async (ori, dest) => {
let origin = ori.latitude+','+ori.longitude;
let destination = encodeURIComponent(dest);
const key = "MyAPIKey";
const URL = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin}&destination=${destination}&key=${key}`;
try{
const response = await fetch(URL)
const route = await response.json()
console.log(route)
return route
}catch(e){
return e
}
}
export default class App extends Component{
state = {
userLocation: null,
route: [],
}
getRouteHandler = (text) => {
fetchRoute(this.state.userLocation, text).then(json => console.log(json));
}
Sometimes if you're fetching large amounts of data it will take awhile for it to log. For example, in a past project the api I was fetching from had close to 5 million records. It took a few minutes to see anything in the console.

Categories