How exactly do I update FlatList's data using Apollo Client's fetchMore method? - react-native

My GraphQL backend returns data in the form:
{
data: {
feed: {
A: [...],
B: [...]
}
}
}
Where my frontend merges A and B using a sort on some field present within the data, and then uses React Native's FlatList to display them as a single array. I'm using Apollo client to query my backend like so:
const { fetchMore, loading, data, error } = useQuery(fetchQuery, {variables: fetchArgs});
if (loading) {...}
if (error) {...}
let A = [];
let B = [];
if (data!.feed.A) A = data!.feed.A
if (data!.feed.B) B = data!.feed.B
let feedData = sortedMerge(A,B);
return (
<FlatList
data={feedData}
...
onEndReached{
//update fetchArgs
fetchMore({
variables: fetchArgs,
updateQuery: // Not sure if I need to do anything here?
}).then(result => {
// Maybe this is where I update?
})
}
)
However, I can't seem to figure out how or where to get the new data, say A' and B' merged and concatenated with feedData so that my FlatList can update as efficiently as possible?
I guess I'm having trouble because I can't just directly update the list and need to do a little bit of preprocessing, but no matter where I update feedData, either in the then block after fetchMore or outside of it, the FlatList never seems to update.

Try putting your merge logic in a useMemo:
const { fetchMore, loading, data, error } = useQuery(fetchQuery, {variables: fetchArgs});
const feedData = useMemo(() => {
if (data) {
const { A, B } = data.feed;
return sortedMerge(A,B);
} else return [];
},[data]);
if (loading) {...}
if (error) {...}
return (
<FlatList
data={feedData}
...
onEndReached = {() => fetchMore({ variables: fetchArgs })}
/>
)
Executing fetchMore should cause data to be updated which will trigger the useMemo and update your feedData variable.
However you'll still need to merge the paginated results into the client-side cache.

Related

Suggestion for fetch the data in database and set progress bar

I have already stored the student id and number of books read in the database. now, I have implemented the method to get the details of books count read by the students. but the function progressbardata() doesn't return the data fetched from query. I have tried to assign a value manually inside the db.transaction() and returned it but no luck.
Could you please let me know where i am doing wrong. also this method looping multiple times. how can we sort this out.
import * as Progress from 'react-native-progress';
import { openDatabase } from 'react-native-sqlite-storage';
var db = openDatabase({ name: 'UserDatabase.db' });
let progressbardata = (id) => {
var totalItems=0;
db.transaction((tx) => {
tx.executeSql('SELECT * FROM student where id = ?', [id], (tx, results) => {
totalItems = results.rows.item(0).percent;
return (totalItems);
})
})
}
const _renderContent = (item, index) => {
return (
<View>
<Progress.Bar color='#68FF33' progress={progressbardata(item.id)}/>
</View>
)
}
My guess would be that the request to fetch the data is asynchronous and this code does not seem to wait for a response to continue. Therefor the code would continue while the fetch requests has not returned a value yet. If this is the case, I would suggest that you let the request set the State of the component, and make the state the input of the Progress.Bar component. This way, when the fetch requests finishes asynchronously, the state will be set and the progress bar will be updated. This would look something like this:
import * as Progress from 'react-native-progress';
import { openDatabase } from 'react-native-sqlite-storage';
import { useState } from 'react'
var db = openDatabase({ name: 'UserDatabase.db' });
const renderContent = (props) => {
[progress, setProgress] = useState(0)
let progressbardata = (id) => {
var totalItems=0;
db.transaction((tx) => {
tx.executeSql('SELECT * FROM student where id = ?', [id], (tx, results) => {
totalItems = results.rows.item(0).percent;
setProgress(totalItems);
})
})
}
progressbardata(props.id)
return (
<View>
<Progress.Bar color='#68FF33' progress={progress}/>
</View>
)
}

How to render text only after a Promise has been resolved in React Native?

I am trying to dynamically translate some text to be displayed when a user clicks on the translate button, but I can't get it to save my values outside of the Promise. I haven't worked much with Promises and every example only shows console.log, rather than saving values outside of the Promise. I don't really understand how they work. Here is (most of) the code I am trying to fix:
constructor(props) {
super(props);
this.state = {
dynamicTranslate: this.props.dynamicTranslate,
};
}
// I've tried this method as both sync and async (with await) but neither work
googleTranslate = (key) => {
const translator = TranslatorFactory.createTranslator();
// translate returns a Promise
return translator.translate(key, i18n.locale)
.then((response) => {return response});
}
renderText() {
// getting some values....
// this loops through all the feedback information
for (var i = 0; i < components_feedback.length; i++) {
let label = (some string);
let value = (some string);
// to do: call google translate call here if Boolean(this.state.dynamicTranslate)
if (Boolean(this.state.dynamicTranslate)) {
// I am ultimately trying to save the translation string from googleTranslate()
// in label/value so I can push it into feedbacks
label = this.googleTranslate(label);
value = this.googleTranslate(value);
}
feedbacks.push({label: label, value: value, type: comp.type})
}
return (
// some stuff
feedbacks.map((feedback, index)) => {
// some stuff
<Text>{feedback.label}</Text>
<Text>{feedback.value}</Text>
// some other stuff
});
);
}
render() {
return (
<View>{this.renderText()}</View>
);
}
One of the issues I'm running into is that label/value is a Promise if translation is on. If I try to make renderText() an async method, it is also turned into a Promise which render() can't handle. No idea where to go from here.
Solved this issue. Solution is to put the loop in an async function that (ideally) gets called on construction. This loop was edited to await the returns and push to local arrays of labels and values then saves those in state. You can compare the length of those arrays to the expected length (compare length of last array being used to be positive that it has finished) and that is how you can know if the Promises have returned. Paraphrased code:
constructor(props) {
this.state = {
translatedLabels = []
translatedValues = []
}
this.asyncFunction()
}
asyncFunction = () => {
labels = []
for loop
label = await promise
labels.push(label)
//same for values
end for
this.setState({translatedLabels: labels})
}
//later
renderText() {
if (this.state.translatedLabels.length === whatever) {
// do your stuff as you know the async function has finished
}
}
render() {
return (
{this.renderText()}
);
}

When state changes for graphql variable, result stays the same on react-native

I'm trying to create an app using shopify graphql api to create an ecommerce app on react native expo.
I have an onPress that calls a setState to change the state of the graphQL variable but the results don't change from the initial state of 'currentSubCategories'
const [currentSubCategories, setSubCategories] = useState(Categories[0].subCategory[0].handle);
let {
collection,
loading,
hasMore,
refetch,
isFetchingMore,
} = useCollectionQuery(currentSubCategories, first, priceRange);
const [currentCategory, setCategory] = useState({categories: Categories[0]});
const onSubCategorySelect = (subCategory) => { setSubCategories(subCategory.handle) }
onPress={() => onSubCategorySelect(item)}
function useCollectionQuery(
collectionHandle: string,
first: number,
priceRange: [number, number],
) {
let [isInitFetching, setInitFetching] = useState<boolean>(true);
let [isReloading, setIsReloading] = useState<boolean>(true);
let [collection, setCollection] = useState<Array<Product>>([]);
let isFetchingMore = useRef<boolean>(false);
let hasMore = useRef<boolean>(true);
let defaultCurrency = useDefaultCurrency().data;
let { data, loading, refetch: refetchQuery } = useQuery<
GetCollection,
GetCollectionVariables
>(GET_COLLECTION, {
variables: {
collectionHandle,
first,
sortKey: ProductCollectionSortKeys.BEST_SELLING,
presentmentCurrencies: [defaultCurrency],
},
notifyOnNetworkStatusChange: true,
fetchPolicy: 'no-cache',
});
let getMoreUntilTarget = async (
targetAmount: number,
cursor: string | null,
handle: string,
filter: [number, number],
) => {
let result: Array<Product> = [];
let moreData: Array<Product> = [];
let { data } = await refetchQuery({
first,
collectionHandle: handle,
after: cursor,
});
...
useEffect(() => {
if (!loading) {
isFetchingMore.current = false;
}
if (isInitFetching && !!data && !!data.collectionByHandle) {
let newCollection = mapToProducts(data.collectionByHandle.products);
hasMore.current = !!data.collectionByHandle?.products.pageInfo
.hasNextPage;
setCollection(newCollection);
setIsReloading(false);
setInitFetching(false);
}
}, [loading, isInitFetching]); // eslint-disable-line react-hooks/exhaustive-deps
return {
collection,
loading: isReloading,
hasMore: hasMore.current,
isFetchingMore: isFetchingMore.current,
refetch,
};
}
I'm using flatList to show the result
<FlatList
data={collection}
renderItem={({ item }) => (
<Text>{item.title}</Text>
)}
/>
According to docs you have to pass new variables to refetch otherwise refetch will use initial values.
In this case (custom hook) you have 2 ways to solvethis problem:
return variables from your custom hook (taken from useQuery),
return some own refetch function.
1st option needs 'manual' variables updating like:
refetch( { ...variablesFromHook, collectionHandle: currentSubCategories } );
In 2nd case you can create myRefetch (and return as refetch) taking collectionHandle parameter to call refetch with updated variables - hiding 'complexity' inside your hook.
Both cases needs refetch call after updating state (setSubCategories) so you should use this refetch inside useEffect with [currentSubCategories] dependency ... or simply don't use state, call refetch directly from event handler (in onSubCategorySelect).

How can I update a nested array with ionic4 storage

Using
openSavedForm() {
this.storage.get('test').then((val) => {
this.auditResults = JSON.parse(val);
this.audit = this.auditResults
this.auditOne = this.auditResults.siteVehicle;
console.log('pull all', this.audit);
});
}
I can view my key value pair stored items in sqlite. Here is a photo of the of the console.log
Is it possible to only update only the siteVehicle Array with
async saveFormUpdates() {
this.newAudit =this.auditOne;
await this.storage.set( 'test', JSON.stringify(this.newAudit));
console.log ("storage", this.newAudit);
}
with out deleting all the other arrays?
My async saveFormUpdates() was wrong. Turns out just saving this.audit instead of this.auditOne did everything with no further input from me.
async saveFormUpdates() {
this.newAudit = this.audit;
await this.storage.set( 'test', JSON.stringify(this.newAudit));
console.log ("storage", this.newAudit);
}

populating a react-select asynchronously

Given a selection made in a react-select (https://jedwatson.github.io/react-select/) I am using axios (https://github.com/mzabriskie/axios) to grab data from a remote url in order to populate a second react-select on the page.
The trick, which is where I think my use case differs from the Async examples, is that I don't want the user to have to type at all into the second select to trigger the auto-populating. I want that auto-population to happen immediately when the data returns from the AJAX call
I have confirmed that I'm grabbing the remote data correctly, but I can't quite figure out the correct syntax for the loadOptions parameter to the react-select. the closest example in the code to what I want is in this function from the project's examples
https://github.com/JedWatson/react-select/blob/master/examples/src/components/GithubUsers.js#L36
thanks for any tips you can offer
It looks like you're trying to do a related select type of scenario, where additional arguments are passed that the second select would use, and you require new options on open.
I did this (with the current build) by overriding AsyncSelect and adjusting my loadOptions.
import Select from 'react-select/lib/Async';
export default class AsyncSelect extends Select {
/**
* reload()
* Called when optional load arguments change, to reload the
* data from remote source with new options
* loadOptions={
* (inputValue) => this.props.loadOptions(
* inputValue,
* this.props.loadArguments
* )
* }
*/
reload() {
this.loadOptions('', options => {
const isLoading = !!this.lastRequest;
this.setState({ defaultOptions: options || [], isLoading });
});
}
componentWillReceiveProps(nextProps) {
// if the cacheOptions prop changes, clear the cache, force a reload
if (nextProps.cacheOptions !== this.props.cacheOptions) {
this.optionsCache = {};
}
/**
* loadArguments
* Optional property used in the remote request.
* If these change externally, then the options should be reloaded.
* This is handy for things like related selects.
*/
if (nextProps.loadArguments !== this.props.loadArguments) {
this.reload();
}
if (nextProps.defaultOptions !== this.props.defaultOptions) {
this.setState({
defaultOptions: Array.isArray(nextProps.defaultOptions)
? nextProps.defaultOptions
: undefined
});
}
}
}
And then, in my component:
const {loadArguments, myLoadFunc, ...attributes} = this.props;
return (
<AsyncSelect
{...attributes}
className={classes}
defaultOptions
loadArguments={loadArguments}
loadOptions={(inputValue) => myLoadFunc(inputValue, loadArguments)}
value={selected}
onChange={this.onChange}
/>
);
In github async example, input parameter getUsers(input) is the search text.
You can store your first react-select value in the component state, than loadOptions promise would fetch endpoint using this state value firstSelectValue
getUsers() {
return fetch(`https://api.github.com/search/users?q=${this.state.firstSelectValue}`)
.then((response) => response.json())
.then((json) => {
return { options: json.items };
});
}