React-select unexpected async behavior - reactjs

There is an unexpected behavior with the react-selectcomponent.
I have it set up to load external json to populate the list.
<Select.Async
name="cf_search"
value=""
autoload={false}
cache={false}
ignoreAccents={false}
loadOptions={this.handleCFSearch}
onChange={this.handleSelectCFName}
/>
handleCFSearch = (input) => {
let term = encodeURIComponent(input);
return fetch(`${AppGlobal.baseBackend}/PersAddo/autocompleteSearch/${term}.json`)
.then((response) => {
if(response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
}).then((json) => {
console.log(json);
let values = json.result.map((element) => {
return {
value: element.pers_id,
label: element.first_name + ' ' + element.last_name
}
});
return { options: values };
});
}
The server script handles the search term and returns an array with the names in JSON.
It works just fine but in some cases it doesn't work.
If I search 'morten twe' the result from the server shows up in the select.
However if I search 'morte twe' (just one character less in the first name) the list does not show up and the select box just looks like not results.
I have tested both search terms and they both return exactly the same JSON:
{
"result": [
{
"pers_id": 123456,
"first_name": "Morten",
"last_name": "Twellmann"
}
]
}
So why doesn't anything show up when the server returns the data correctly?

react-select does some client-side filtering by default. So even though your API is returning the value, react-select filters it out. The default filter implementation it uses can be found here . You can see the filterOptions prop being called here in Select.js, which Async utilizes.
The default filtering is basically just checking for substring, case-insensitive equality. In the case where the value is not shown, there is a difference between the search and the result, so it is filtered out.
You can provide your own filterOptions function to override the default, or just pass undefined to turn off the clientside filtering altogether.

Related

Velo by Wix, JSON data in repeater

I'm trying to get the temperature of each hour from this website: https://www.smhi.se/vader/prognoser/ortsprognoser/q/Stockholm/2673730
I'm getting the data from https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/16/lat/58/data.json. The "t" object is the temperature.
The problem I have is displaying the data for each hour in the repeater.
Here is my backend-code:
import { getJSON } from 'wix-fetch';
export async function getWeather() {
try {
const response = await getJSON('https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/16/lat/58/data.json');
console.log(response) // all data
const tempData = response.timeSeries[0].parameters[10].values[0];
return tempData // Only returns "t" - temperature
} catch (e) {
return e;
}
}
The backend part works, however the frontend doesn't.
import { getWeather } from 'backend/getSMHI.jsw'
$w.onReady(function () {
(
getWeather().then(weatherInfo => {
$w('#weatherRepeater').onItemReady(($item, itemData, index) => {
if (index > 6) {
$item('#tempText').text = itemData.timeSeries[index].parameters[1].values[0];
} else if (index === 6) {
$item('#tempText').text = itemData.timeSeries[index].parameters[0].values[0];
} else {
$item('#tempText').text = itemData.timeSeries[index].parameters[10].values[0];
} // The parameters number for "t" changes depending on the index
})
$w('#weatherRepeater').data = weatherInfo;
})
)
})
Seems like there are at least a couple of issues here.
First, you are retrieving a single number from the API and trying to put that in a repeater. From the description of what you're trying to do, it would seem that you mean to be retrieving a list of numbers, probably as an array. You probably want to do some filtering and/or mapping on the response data instead of directly accessing a single value.
Second, the data you send to a repeater must be in the proper format. Namely, it must be an array of objects, where each object has a unique _id property value (as a string). You are not doing that here. You are simply assigning it a number.
Third, and this is just an efficiency thing, you don't need to define the onItemReady inside the then(). Not that it will really make much of a difference here.

react-admin calls getList twice on sorting

I have a Datagrid inside a List with custom sorting and filtering. Everything works fine except that when the function that updates the sorting is called, the getList method is called twice (the server is called twice, which is very time consuming).
Here is the fucntion responsible of updating sorting field and order:
const handleSort = (key) => {
console.log("key for sorting :", key);
const {field, order} = sort;
console.log("field and order: ", field +" "+order);
if(field === key) {
sort.order = order === "ASC" ? "DESC" : "ASC";
} else {
sort.field = key;
}
setSort(sort);
refresh();
}
What could probably be the cause of this ?
Edit:
Actually I found that every simple click anywhere on the web page causes react-admin to call getList with the current url (to refresh the page). Is there a way to disable this behaviour or to control it ?
The first request was actually done on windows focus as dictated by React pareameter refetchOnWindowFocus, which has the value true by default.
I changed its value to false in the queryClient prop of the Admin object like the following:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 10000,
retry: false,
refetchOnWindowFocus: false,
},
},
});
return <Admin
queryClient={queryClient} />
...
</Admin >

Updating multiple queries in useMutation Apollo-client without refetching them

I'm executing this mutation in my NewBook component:
const [addBook] = useMutation(ADD_BOOK, {
update: (cache, response) => {
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
},
refetchQueries: [{ query: ALL_AUTHORS }, { query: ALL_GENRES }],
options: {
awaitRefetchQueries: true,}});
Instead of having to refetch those two queries, I'd like to update them like ALL_BOOKS - but could not find any example in the docs. Does anyone know a way to accomplish that?
Thank you.
What you need to do is make multiple cache updates based on response data.
Once you add your new book to the query, the next step is to fetch all authors.
cache.updateQuery({ query: ALL_BOOKS }, ({ allBooks }) => {
return { allBooks: allBooks.concat(response.data.addBook) };
});
//Get all authors
const existingAuthors = cache.readQuery({
query: ALL_AUTHORS,
//variables: {}
});
//If we never called authors, do nothing, as the next fetch will fetch updated authors. This might be a problem in the future is some cases, depending on how you fetch data. If it is a problem, just rework this to add newAuthor to the array, like allAuthors: [newAuthor]
if(!existingAuthors.?length) {
return null
}
The next thing is that we need to compare the new book's author with existing authors to see if a new author was added.
//continued
const hasAuthor = existingAuthors.find(author => author.id === response.data.createBook.id)
//Double check response.data.createBook.id. I don't know what is returned from response
//If author already exists, do nothing
if(hasAuthor) {
return null
}
//Get a new author. Make sure that this is the same as the one you fetch with ALL_AUTHORS.
const newAuthor = {
...response.data.createBook.author //Check this
}
cache.writeQuery({
query: ALL_AUTHORS,
//variables: {}
data: {
allAuthors: [newAuthor, ...existingAuthors.allAuthors]
},
});
Then continue the same with ALL_GENRES
Note:
If you called ALL_GENERES or ALL_BOOKS with variables, you MUST put the SAME variables in the write query and read query. Otherwise Apollo wont know what to update
Double check if you are comparing numbers or strings for authors and genres
Double check all of the variables I added, they might be named different at your end.
Use console.log to check incoming variables
You can probably make this in less lines. There are multiple ways to update cache
If it doesn't work, console.log cache after the update and see what exactly did apollo do with the update (It could be missing data, or wrong variables.)
Add more checks to ensure some cases like: response.data returned null, authors already fetched but there are none, etc...

Angular/Firestore Collection Document Query to return a single document field from all documents into an array

I am performing a query on my collection documents and trying to return just all phone numbers into an array. I just want to set the phone numbers into array for use by another function. Firebase docs only show a console log for (doc.id) and (doc.data) and no practical use for any other objects in your documents. My console log for info.phoneNumbers returns all the phoneNumbers.
async getPhone() {
await this.afs.collection('members', ref => ref.where('phoneNumber', '>=', 0))
.get().toPromise()
.then(snapshot => {
if (snapshot.empty) {
console.log('No Matches');
return;
}
this.getInfo(snapshot.docs);
});
}
getInfo(data) {
data.forEach(doc => {
let info = doc.data();
console.log(info.phoneNumber, 'Phonenumbers');
// let myArray = [];
// myArray.push(doc.doc.data());
// const phoneNumber = info.phoneNumber as [];
// console.log(myArray, 'ARRAY');
return info.phoneNumber;
})
}```
Firestore is a "document store database". You fetch and store entire DOCUMENTS (think "JSON objects") at a time. One of the "anti-patterns" when using document store databases is thinking of them in SQL/relational DB terms. In SQL/relational DB, you "normalize" data. But in a document store database (a "NoSQL" database) we explicitly denormalize data -- that is, we duplicate data -- across documents on write operations. This way, when you fetch a document, it has all the data you need for its use cases. You typically want to avoid "JOINs" and limit the number of references/keys in your data model.
What you are showing in the code above is valid in terms of fetching documents, and extracting the phoneNumber field from each. However, use of .forEach() is likely not what you want. forEach() iterates over the given array and runs a function, but the return value of forEach() is undefined. So the return info.phoneNumber in your code is not actually doing anything.
You might instead use .map() where the return value of the map() function is a new array, containing one entry for each entry of the original array, and the value of that new array is the return value from map()'s callback parameter.
Also, mixing await and .then()/.catch() is usually not a good idea. It typically leads to unexpected outcomes. I try to use await and try/catch, and avoid .then()/.catch() as much as possible.
So I would go with something like:
try {
let querySnap = await this.afs.collection('members', ref =>
ref.where('phoneNumber', '>=', 0)).get();
let phoneNumbers = await this.getInfo(querySnap.docs[i].data());
} catch(ex) {
console.error(`EXCEPTION: ${ex.message}`);
}
getInfo(querySnapDocs) {
let arrayPhoneNumbers = querySnapDocs.map(docSnap => {
let info = doc.data();
let thePhoneNum = info.phoneNumber
console.log(`thePhoneNum is: ${thePhoneNum}`);
return thePhoneNum;
});
return arrayPhoneNumbers;
});
I solved this with help and I hope this may be helpful to others in Getting access to 1 particular field in your documents. In my service:
async getPhone() {
return await this.afs.collection('members', ref => ref.where('phoneNumber', '>=', 0))
.get().toPromise()
.then(snapshot => {
if (snapshot.empty) {
console.log('No Matches');
return;
}
return this.getInfoNum(snapshot.docs);
});
}
getInfoNum(data) {
return data.map(doc => {
let info = doc.data();
return info.phoneNumber
});
}
In my Component using typescript
phoneNumbers: string[] = [];
getPhone() {
this.dbService.getPhone().then(phoneNumbers => {
this.phoneNumbers = phoneNumbers;
this.smsGroupForm.controls.number.setValue(phoneNumbers.join(',')) //sets array seperated by commas
console.log(phoneNumbers);
});
}
This returns all the phone numbers in a comma separated array.
In my template I pull the numbers into an input for another function to send multiple text. Code in the template is not polished yet for the form, I am just getting it there for now.
<ion-list>
<ion-item *ngFor="let phoneNumber of phoneNumbers">
<ion-label position="floating">Phone Number</ion-label>
<ion-input inputmode="number"
placeholder="Phone Number"
formControlName="number"
type="number">{{ phoneNumber }}
</ion-input>
</ion-item>
</ion-list>

How to make an infinite scroll with react-query?

I'm trying to use react-query useInfiniteScroll with a basic API, such as the cocktaildb or pokeapi.
useInfiniteQuery takes two parameters: a unique key for the cache and a function it has to run.
It returns a data object, and also a fetchMore function. If fetchMore is called - through an intersection observer for exemple -, useInfiniteQuery call its parameter function again, but with an updated payload thanks to a native callback getFetchMore().
In the official documentation, getFetchMore automatically takes two argument: the last value returned, and all the values returned.
Based on this, their demo takes the value of the previous page number sent by getFetchMore, and performs a new call with an updated page number.
But how can I perform the same kind of thing with a basic api that only return a json?
Here is the official demo code:
function Projects() {
const fetchProjects = (key, cursor = 0) =>
fetch('/api/projects?cursor=' + cursor)
const {
status,
data,
isFetching,
isFetchingMore,
fetchMore,
canFetchMore,
} = useInfiniteQuery('projects', fetchProjects, {
getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
})
infinite scrolling relies on pagination, so to utilize this component, you'd need to somehow track what page you are on, and if there are more pages. If you're working with a list of elements, you could check to see if less elements where returned in your last query. For example, if you get 5 new items on each new fetch, and on the last fetch you got only 4, you've probably reached the edge of the list.
so in that case you'd check if lastGroup.length < 5, and if that returns true, return false (stop fetching more pages).
In case there are more pages to fetch, you'd need to return the number of the current page from getFetchMore, so that the query uses it as a parameter. One way of measuring what page you might be on would be to count how many array exist inside the data object, since infiniteQuery places each new page into a separate array inside data. so if the length of data array is 1, it means you have fetched only page 1, in which case you'd want to return the number 2.
final result:
getFetchMore: (lastGroup, allGroups) => {
const morePagesExist = lastGroup?.length === 5
if (!morePagesExist) return false;
return allGroups.length+1
}
now you just need to use getMore to fetch more pages.
The steps are:
Waiting for useInfiniteQuery to request the first group of data by default.
Returning the information for the next query in getNextPageParam.
Calling fetchNextPage function.
Reference https://react-query.tanstack.com/guides/infinite-queries
Example 1 with rest api
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
data,
isLoading,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage) => {
// lastPage signature depends on your api respond, below is a pseudocode
if (lastPage.hasNextPage) {
return lastPage.nextCursor;
}
return undefined;
},
})
Example 2 with graphql query (pseudocode)
const {
data,
fetchNextPage,
isLoading,
} = useInfiniteQuery(
['GetProjectsKeyQuery'],
async ({ pageParam }) => {
return graphqlClient.request(GetProjectsQuery, {
isPublic: true, // some condition/variables if you have
first: NBR_OF_ELEMENTS_TO_FETCH, // 10 to start with
cursor: pageParam,
});
},
{
getNextPageParam: (lastPage) => {
// pseudocode, lastPage depends on your api respond
if (lastPage.projects.pageInfo.hasNextPage) {
return lastPage.projects.pageInfo.endCursor;
}
return undefined;
},
},
);
react-query will create data which contains an array called pages. Every time you call api with the new cursor/page/offset it will add new page to pages. You can flatMap data, e.g:
const projects = data.pages.flatMap((p) => p.projects.nodes)
Call fetchNextPage somewhere in your code when you want to call api again for next batch, e.g:
const handleEndReached = () => {
fetchNextPage();
};
Graphql example query:
add to your query after: cursor:
query GetProjectsQuery($isPublic: Boolean, $first: Int, $cursor: Cursor) {
projects(
condition: {isPublic: $isPublic}
first: $first
after: $cursor
) ...

Resources