Apollo-Client refetch - TypeError: undefined is not an object - react-native

I have a flatlist in react-native and I am trying to refetch the data when pulling it down (the native refresh functionality). When I do, I am getting this error:
Typeerror: undefined is not an object
I can't figure out what is going wrong. I am using
Expo SDK 38
"#apollo/client": "^3.1.3",
"graphql": "^15.3.0",
This is my code:
export default function DiscoverFeed({ navigation }) {
const theme = useTheme();
const { data, error, loading, refetch, fetchMore, networkStatus } = useQuery(
GET_RECIPE_FEED,
{
variables: { offset: 0 },
notifyOnNetworkStatusChange: true,
}
);
if (error) return <Text>There was an error, try and reload.</Text>;
if (loading) return <Loader />;
if (networkStatus === NetworkStatus.refetch) return <Loader />;
const renderItem = ({ item }) => {
return (
<View style={styles.cardItems}>
<RecipeCard item={item} navigation={navigation} />
</View>
);
};
return (
<SafeAreaView style={styles.safeContainer} edges={["right", "left"]}>
<FlatList
style={styles.flatContainer}
data={data.recipe}
removeClippedSubviews={true}
renderItem={renderItem}
refreshing={loading}
onRefresh={() => {
refetch();
}}
keyExtractor={(item) => item.id.toString()}
onEndReachedThreshold={0.5}
onEndReached={() => {
// The fetchMore method is used to load new data and add it
// to the original query we used to populate the list
fetchMore({
variables: {
offset: data.recipe.length,
},
});
}}
/>
</SafeAreaView>
);
}
I have a typepolicy like so:
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
recipe: {
merge: (existing = [], incoming, { args }) => {
// On initial load or when adding a recipe, offset is 0 and only take the incoming data to avoid duplication
if (args.offset == 0) {
console.log("offset 0 incoming", incoming);
return [...incoming];
}
console.log("existing", existing);
console.log("incoming", incoming);
// This is only for pagination
return [...existing, ...incoming];
},
},
},
},
},
});
And this is the query fetching the data:
export const GET_RECIPE_FEED = gql`
query GetRecipeFeed($offset: Int) {
recipe(order_by: { updated_at: desc }, limit: 5, offset: $offset)
#connection(key: "recipe") {
id
title
description
images_json
updated_at
dishtype
difficulty
duration
recipe_tags {
tag {
tag
}
}
}
}
`;

Related

React Native dynamic search with flatlist from API

function ManageData({props, navigation}) {
const [details, setDetails] = useState({
dataList: [],
loading: true,
offset: 1,
totalRecords: 0,
search: '',
});
useEffect(() => {
getData();
}, []);
const getData = async () => {
try {
// console.log('search',details.search);
var params = {};
params = {
'pagination[page]': details.offset,
'pagination[perpage]': 10,
};
if(details?.search?.length > 0){
params['query[search]'] = details?.search;
params['pagination[pages]'] = 30;
params['pagination[total]'] = 293;
}else{
params['query'] = ""
}
const result = await getPayeeDetails(session, params);
// console.log('result',result?.data?.data?.length);
if (result?.data?.data?.length > 0) {
setDetails(prev => ({
...prev,
offset: prev.offset + 1,
dataList: [...prev.dataList, ...result.data.data],
loading: false,
totalRecords: result.data.recordsFiltered,
}));
}
} catch (error) {
console.log('getPayeesError', error);
}
};
const loadMore = () => {
try {
if (details.dataList.length != details.totalRecords) {
setDetails(prev => ({
...prev,
loading: true,
}));
getData();
}
} catch (error) {
console.log('LoadMoreError', error);
}
};
const searchHandler=(data)=>{
try{
console.log('clearData',data);
setDetails(prev => ({
...prev,
dataList:[],
offset:1,
search: data == 'RESET'?"":data,
}));
getData();
}catch(error){
console.log("SearchError",error)
}
}
return (
<BackDropContainer
searchHandler={searchHandler}>
<View style={{backgroundColor: 'white', flex: 1}}>
<FlatList
style={{marginTop: '4%'}}
data={details?.dataList}
renderItem={({item}) => (
<TouchableOpacity onPress={() => showDialog(item)}>
<Item data={item} />
</TouchableOpacity>
)}
onEndReached={loadMore}
keyExtractor={(item, index) => index}
/>
</View>
</BackDropContainer>
);
}
I have a flatlist with searchview in my React Native application. Each time user scrolls to the end of flatlist the loadmore function will be called and also the offset value is increased as 1 to fetch next page from API.
Every time the API results array of 10 data from API so the flatlist will be loaded 10 by 10 for each scroll. When I type some data in searchview the searchHandler function will be called, and there I want to reset the offset as 1 and also need to send typed data to the API.
The issue is searched data and offset is not sending with API whenever I try to search the data. State is not updating properly when searching data.
Note: The data which is types has to be sent along with API whenever user search something.

How to force update single component react native

I'm using 2 usestate in my component
const [choosedH, setChoosedH] = useState([]);
const [h, setH] = useState([]);
I have async method which fetch data from api and convert it to final array.
useEffect(() => {
getH();
}, [])
async function getH(){
const username = await SecureStore.getItemAsync('username')
const token = await SecureStore.getItemAsync('token')
axiosInstance.get('/api/xxx/' + username,
{
headers: {
Cookie: token,
},
},
{ withCredentials: true }
)
.then((response) => {
if(response.data.length > 0){
let singleH = {};
response.data.forEach(element => {
singleH = {
label: element.name,
value: element.name
}
h.push(singleH);
});
console.log(h)
}
})
.catch(function (error) {
console.log('There has been a problem with your fetch operation: ' + error.message);
throw error;
})
}
and finally i have my component:
<RNPickerSelect
onValueChange={(value) => setChoosedH(value)}
items={h}
useNativeAndroidPickerStyle={false}
style={{
...pickerSelectStyles,
iconContainer: {
top: 10,
right: 10,
},
}}
placeholder={{
label: 'Select',
value: null,
}}
Icon={() => {
return <Icon name="arrow-down" size={24} />;
}}
value={choosedH}
/>
I have a problem. After render my picker contain empty array. When render end, hook useEffect call getH() which give me data from api and convert it as I want to value of useState "h". How to force update picker items when function getH will end? It it possible to get data from api before render? Any ideas ?
I guess the problem is that you try to access h directly instead of using setH.
This should work:
if(response.data.length > 0){
const myArray = []
response.data.forEach(element => {
const singleH = {
label: element.name,
value: element.name
}
myArray.push(singleH);
});
setH(myArray)
console.log(h)
}

Apollo cache query fields policy offsetLimitPagination() doesnt work with subscriptions

I use apollo client for react native.
When I use offsetLimitPagination() for pagination my subscriptions doesn't update cache.
Subscriptions works correctly but doesn't update flatlist data.
When i remove offsetLimitPagination function it works. I can't use together subscriptions and offsetLimitPagination function on cache.
Is there any solution for that?`
Thanks.
Cache
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
chatDetail: offsetLimitPagination(),
}
}
},
});
ChatDetailPage
import React, { useState, useCallback } from 'react'
import { StyleSheet, Text, View, FlatList } from 'react-native'
import { ActivityIndicator } from 'react-native-paper';
import { useQuery } from '#apollo/client'
import { useSelector } from 'react-redux'
import { CHAT_DETAIL } from '../../../Graphql/Queries/Message'
import { MESSAGE_SUB } from '../../../Graphql/Subscriptions/Message'
import MainFlow from './Components/Flow/MainFlow'
const ChatDetailMain = () => {
const user = useSelector(state => state.auth.user)
const currentRoom = useSelector(state => state.room.currentRoom)
const [hasNext, setHasNext] = useState(true)
const limit = 15
const { error, loading, data, refetch, fetchMore, subscribeToMore } = useQuery(CHAT_DETAIL, {
variables: { userId: user._id, roomId: currentRoom._id, limit }, fetchPolicy: "cache-and-
network",
nextFetchPolicy: "cache-first" })
// render item
const renderItem = (
({item} ) => {
return <MainFlow item={item} />
}
)
if (error) {
console.warn('CHAT_DETAIL QUERY ERROR: ', error)
console.log(error.message);
return (
<View>
<Text>
An Error Occured: {error.message}
</Text>
</View>
)
}
if (loading || data == undefined || data == null) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size={50} color="gray" />
</View>
)
}
// fetchMore
const fetchMoreData=()=>{
// console.log("fetchMore runnig hasnext limit, data.chatDetail.length >= limit ", hasNext, limit, data.chatDetail.length >= limit);
if(hasNext && data.chatDetail.length >= limit){
fetchMore({
variables:{
offset: data.chatDetail.length,
limit: data.chatDetail.length+limit
}
}).then((flowMoredata)=>{
if(flowMoredata.data.chatDetail.length==0 || flowMoredata.data.chatDetail.length === data.chatDetail.length){
setHasNext(false)
}
})
}
}
// subscription area
const subscribeQ = () => subscribeToMore({
document: MESSAGE_SUB,
variables: {
userId: user._id
},
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev;
const { messageSub } = subscriptionData.data
let current = {}
let others = []
switch(messageSub.type){
case 'update':
prev.chatDetail.map(message => {
if (message._id != messageSub.message._id) others.push(message)
if (message._id == messageSub.message._id) current = messageSub.message
})
return { chatDetail: [ current, ...others ]}
case 'remove':
prev.chatDetail.map(message => {
if (message._id != messageSub.message._id) others.push(message)
if (message._id == messageSub.message._id) current = messageSub.message
})
return { chatDetail: [ ...others ]}
case 'create':
return { chatDetail: { ...prev, chatDetail: [ messageSub.message, ...prev.chatDetail] }}
default: return { ...prev }
}
}
})
if (subscribeToMore != undefined && subscribeToMore) {
subscribeQ()
}
return (
<View>
<FlatList
data={data.chatDetail}
renderItem={renderItem}
keyExtractor={(item, index) => String(index)}
onEndReached={fetchMoreData}
onEndReachedThreshold={0.2}
contentContainerStyle={{ paddingTop: 80 }}
inverted={true}
/>
</View>
)
}
export default ChatDetailMain
const styles = StyleSheet.create({})
It was about cache merge issue. If you want to cache data, you shoul give a key to apollo client "Cache according to the what, for each room has an id or roomName for keyArgs param it should uniqe value like that
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
chatDetail: {
keyArgs:['roomId'],
merge(incoming=[], existing=[]){
.
.
.
return offsetLimitPagination()
}}
}
}
},
});

React Native Flat List doesn't call onEndReached handler after two successful calls

I implement a very simple list that calls a server that returns a page containing books.Each book has a title, author, id, numberOfPages, and price). I use a Flat List in order to have infinite scrolling and it does its job very well two times in a row (it loads the first three pages) but later it doesn't trigger the handler anymore.
Initially it worked very well by fetching all available pages, but it stopped working properly after I added that extra check in local storage. If a page is available in local storage and it has been there no longer than 5 seconds I don't fetch the data from the server, instead I use the page that is cached. Of course, if there is no available page or it is too old I fetch it from the server and after I save it in local storage.(Something went wrong after adding this behavior related to local storage.)
Here is my component:
export class BooksList extends Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 0
};
}
async storePage(page, currentTime) {
try {
page.currentTime = currentTime;
await AsyncStorage.setItem(`page${page.page}`, JSON.stringify(page));
} catch (error) {
console.log(error);
}
}
subscribeToStore = () => {
const { store } = this.props;
this.unsubsribe = store.subscribe(() => {
try {
const { isLoading, page, issue } = store.getState().books;
if (!issue && !isLoading && page) {
this.setState({
isLoading,
books: (this.state.books ?
this.state.books.concat(page.content) :
page.content),
issue
}, () => this.storePage(page, new Date()));
}
} catch (error) {
console.log(error);
}
});
}
componentDidMount() {
this.subscribeToStore();
// this.getBooks();
this.loadNextPage();
}
componentWillUnmount() {
this.unsubsribe();
}
loadNextPage = () => {
this.setState({ pageNumber: this.state.pageNumber + 1 },
async () => {
let localPage = await AsyncStorage.getItem(`page${this.state.pageNumber}`);
let pageParsed = JSON.parse(localPage);
if (localPage && (new Date().getTime() - localPage.currentTime) < 5000) {
this.setState({
books: (
this.state.books ?
this.state.books.concat(pageParsed.content) :
page.content),
isLoading: false,
issue: null
});
} else {
const { token, store } = this.props;
store.dispatch(fetchBooks(token, this.state.pageNumber));
}
});
}
render() {
const { isLoading, issue, books } = this.state;
return (
<View style={{ flex: 1 }}>
<ActivityIndicator animating={isLoading} size='large' />
{issue && <Text>issue</Text>}
{books && <FlatList
data={books}
keyExtractor={book => book.id.toString()}
renderItem={this.renderItem}
renderItem={({ item }) => (
<BookView key={item.id} title={item.title} author={item.author}
pagesNumber={item.pagesNumber} />
)}
onEndReachedThreshold={0}
onEndReached={this.loadNextPage}
/>}
</View>
)
}
}
In the beginning the pageNumber available in the state of the component is 0, so the first time when I load the first page from the server it will be incremented before the rest call.
And here is the action fetchBooks(token, pageNumber):
export const fetchBooks = (token, pageNumber) => dispatch => {
dispatch({ type: LOAD_STARTED });
fetch(`${httpApiUrl}/books?pageNumber=${pageNumber}`, {
headers: {
'Authorization': token
}
})
.then(page => page.json())
.then(pageJson => dispatch({ type: LOAD_SUCCEDED, payload: pageJson }))
.catch(issue => dispatch({ type: LOAD_FAILED, issue }));
}
Thank you!

Apollo Graphql Pagination failed with limit

I am trying out Apollo pagination. It works correctly if I do not pass the limit argument from the client and hard code the limit argument in my hasMoreData function. If I were to add in the limit argument, all the data will be returned from my server and it will not paginate. The server side code should be correct (I tested it on GraphQL playground).
This does not work properly:
import React, { Component } from "react";
import {
View,
Text,
ActivityIndicator,
FlatList,
Button,
StyleSheet
} from "react-native";
import { graphql } from "react-apollo";
import gql from "graphql-tag";
let picturesList = [];
class HomeScreen extends Component {
loadMore = () => {
this.props.data.fetchMore({
variables: {
offset: picturesList.length
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prev;
}
return {
...prev,
pictures: [...prev.pictures, ...fetchMoreResult.pictures]
};
}
});
};
render() {
const { loading, pictures, variables } = this.props.data;
picturesList = pictures;
if (loading) {
return <ActivityIndicator size="large" />;
}
//TODO - hard coded the limit as 3 which is not supposed to
let hasMoreData = picturesList.length % 3 === 0;
if (picturesList.length <= variables.offset) {
hasMoreData = false;
}
return (
<View style={styles.root}>
<Button title="Show More" onPress={this.loadMore} />
<FlatList
data={picturesList}
renderItem={({ item }) => (
<View style={styles.contentContainer}>
<Text style={styles.content}>{item.title}</Text>
</View>
)}
keyExtractor={item => item.id}
ListFooterComponent={() =>
hasMoreData ? (
<ActivityIndicator size="large" color="blue" />
) : (
<View />
)
}
/>
</View>
);
}
}
const styles = StyleSheet.create({
root: {
flex: 1
},
content: {
fontSize: 35
},
contentContainer: {
padding: 30
}
});
// adding the limit variable here will cause my server to return all data
const PICTURES_QUERY = gql`
query($offset: Int, $limit: Int) {
pictures(offset: $offset, limit: $limit) {
id
title
pictureUrl
}
}
`;
export default graphql(PICTURES_QUERY)(HomeScreen);
The server-side code, just in case:
pictures: async (_, { offset, limit }) => {
let picturesDB = getConnection()
.getRepository(Picture)
.createQueryBuilder("p");
return picturesDB
.take(limit)
.skip(offset)
.getMany();
}
I have added a default parameter in my GraphQL schema:
type Query {
pictures(offset: Int, limit: Int = 3): [Picture!]!
}
Managed to pass the limit variable using Apollo HOC pattern...
export default graphql(PICTURES_QUERY, {
options: () => ({
variables: {
limit: limitAmt
}
})
})(HomeScreen);