Mykhailo Kondrat
Mykhailo Kondrat

Reputation: 133

ApolloClient V3 with React: Caching and re-using result of fetchMore query

I have a small learning project build with React, TS, & Apollo. As backend I am using https://graphqlzero.almansi.me/

The result I am looking for is:

  1. Fetch Posts by page, 5 per page
  2. Re-use previously fetched data

Here is my container

export const GET_POSTS = gql`
 
    query GetPosts($options: PageQueryOptions!) {
        posts(options: $options) {
            data {
                id
                title
                body
            }
        }
    }
`


const Posts = (): JSX.Element => {
    const [page, setPage] = useState<number>(1)
    const { data, fetchMore} = useQuery(GET_POSTS, {
        variables: {
            options: {
                paginate: {
                    page: 1,
                    limit: 5,
                },
            },
        },
            nextFetchPolicy: "cache-first"
    },
        )
    return (
        <div>
            <ListOfPosts {...{data,fetchMore,page,setPage }} />
        </div>
    )
}

and ListOfPosts

const ListOfPosts = ({ data,fetchMore, page, setPage }) => {
    const getNextPage = (): void => {
        fetchMore({
            variables: {
                options: {
                    paginate: {
                        page: page + 1,
                        limit: 5,
                    },
                },
            },
        })
        setPage((p: number) => p + 1)
    }

    const getPrevPage = (): void => {
        setPage((p: number) => (p === 0 ? p : p - 1))
    }
    console.log(data)
    return (
        <div>
            <p>current page{page}</p>
            <button type="button" onClick={getPrevPage}>
                Get Prev Page
            </button>
            <button type="button" onClick={getNextPage}>
                Get Next Page
            </button>
            {data &&
                data?.posts?.data?.map((post: any) => (
                    <div key={post.id}>
                        <h4>{post.title}</h4>
                        <p>{post.body}</p>
                    </div>
                ))}
        </div>
    )

So if I send a query with page === 1 I get posts from 1 to 5, if page === 2 - posts from 6 to 10 and so on.

The problem is that if for example I send request in next sequence

  1. page === 1 (initially sent by useQuery)
  2. page === 2 ( sending with fetchMore)
  3. page === 3 ( sending with fetchMore)
  4. page === 2 ( sending with fetchMore)

on last request Apollo performs network request, despite data for that that request is already in cache So my questions actually are:

  1. how to configure Apollo cache to return required data without re-fetching it from a server
  2. How to "invalidate" that data and tell Apollo that I need refresh that particular portion of data?

I think it should be somehow configured in cache typePolicies but haven't found a way to make it work - despite data is in cache( I can track it with browser extension) it is not returned in {data}=useQuery :/ Here is how my cache config looks like.

export const cache = new InMemoryCache({
    typePolicies: {
        Query: {
            fields: {
                posts: {
                    merge(existing, incoming) {
                        return [ ...existing,...incoming ]
                    }
                }
            }
        }
    }
})

Upvotes: 0

Views: 1039

Answers (1)

Herku
Herku

Reputation: 7666

So there are two types of pagination. In the first pagination, the "pages" are somehow known to the user and the user navigates through these via the UI. This seems to be what you want to do:

I would propose to keep it simple: Change the state variables, pass them into useQuery and render the results. Apollo will run the query again with the new page value every time the variables change. If Apollo has seen the same variables before (you go back to the previous page), Apollo can return the result from cache.

export const GET_POSTS = gql`
    query GetPosts($options: PageQueryOptions!) {
        posts(options: $options) {
            data {
                id
                title
                body
            }
        }
    }
`


const Posts = (): JSX.Element => {
    const [page, setPage] = useState<number>(1)
    const { data } = useQuery(GET_POSTS, {
        variables: {
            options: {
                paginate: {
                    page,
                    limit: 5,
                },
            },
        },
        nextFetchPolicy: "cache-first"
    })

    return (
        <div>
            <ListOfPosts {...{data,page,setPage }} />
        </div>
    )
}

const ListOfPosts = ({ data, page, setPage }) => {
    const getNextPage = (): void => setPage((p: number) => p + 1);
    const getPrevPage = (): void =>
        setPage((p: number) => (p === 0 ? p : p - 1))

    return (
        <div>
            <p>current page{page}</p>
            <button type="button" onClick={getPrevPage}>
                Get Prev Page
            </button>
            <button type="button" onClick={getNextPage}>
                Get Next Page
            </button>
            {data &&
                data?.posts?.data?.map((post: any) => (
                    <div key={post.id}>
                        <h4>{post.title}</h4>
                        <p>{post.body}</p>
                    </div>
                ))}
        </div>
    )
}

Merging the cache is only relevant when you want to continuously show results after another. Things like "load more" or "infinite scrolling". This means you want to continue adding to your result list. You would add more and more posts to the view and the old pages don't disappear. This is what fetchMore is designed for. If you want to do that have a look at the docs and this part specifically. Your problem is that you are currently mixing both approaches, which probably leads to weird results.

Upvotes: 2

Related Questions