Swr: Is there a way to reset pages of `useSWRPages`?

Created on 9 Dec 2019  Β·  27Comments  Β·  Source: vercel/swr

Hi

I'm using swr for pagination in my local next.js project. My implementation is based on this example.
The difference is that my component with useSWRPages is using dynamic route, which loads user's feed, and the route looks like this: /user/[username].

The problem appears when I switch user feed with loaded pages. In more details:

  1. I, as a user, open user-A's feed, then load a number of pages (let's say 4).
  2. I change feed to user-B's.
  3. User-B's feed loads 4 pages, instead of 1.

I'd tried to pass username in to useSWRPages's deps, and use it in key, but it didn't helped me. Is there any workarounds? Maybe I should pass something else in to the deps?

There's is an issue that seems like similar to mine: #126.

feature request pagination

Most helpful comment

I think I found a way.
You need to pass the filter key from top component to page component, which will be used as the page key and as the react key. As a result, when changing the key, the component will be rerendered and SWR will get cache for current pageKey.

For example:

// Main.tsx
...
const [currentTag, setCurrentTag] = useState('tag-1');
...

return (
  <>
    <Filter tag={currentTag} onTagChange={setCurrentTag} />
    <Content key={currentTag} tag={currentTag}  /> // it's important to set `key` here
  </>
)
// Content.tsx

const Content = ({ tag }) => {
  const { pages } = useSWRPages(
    pageKey: tag, // tag as pageKey here
    ...

  return <>{pages}</>
}

All 27 comments

I'm experiencing the same issue using a code also based on that example. On top of that, one loader is displayed for each page that's being loaded, which doesn't make a great UI.

Currently you can try

mutate('_swr_page_count_' + pageKey, 0)
mutate('_swr_page_offset_' + pageKey, 0)

as a temporary work around.

I will work on simplifying the useSWRPages API next week since it's tricky to get it right (conditional and dependent hooks) and we have several bugs/feature requests around it. πŸ™

thank you @quietshu, but calling mutate didn't helped :(
however, i'm using useSWR with axios with global fetch, so maybe I messed up somewhere.

Also, offset being null on initial load, and on reaching-end is causing some problems, I made next-page-offset-getter return undefined instead of null, to distinguish between initial page and last page, but it broke useSWRPages's states. I ended up injecting couple of my own states (isLoading, isEmpty) into the page component, which helped to hide page loaders away, so now I have one outside loader for all pages.

@tem-tem I'm having the same issue, did you manage to find a workaround?

Ok, I made it work. These are the changes I applied https://github.com/gabrielperales/swr/commit/8ba8379d2e6c580ded234385d34ea829be996788 . If somebody is having the same issue and want to give it a try they can check my branch running npm i -save gabrielperales/swr#build. I'm not making a PR because seems that the API is changing and @quietshu is working on it. @quietshu if you think I should make a PR let me know which changes I should do in order to make it able to be approved.

Thanks @gabrielperales - that fixes the issue when pageKey changes, as otherwise the class just loads pageCount, pageOffsets once from the cache using useState on initial render, so subsequent changes affect all pageKey values.

I also noticed incorrect values being returned by isReachingEnd because sometimes undefined is returned, not just null.

For now I just modified the check to test using double equals:

const isReachingEnd = pageOffsets[pageCount] == null

This resolves the issues of load more / spinner classes incorrectly displaying on front-ends using code based on the swr examples.

I think I have the same problem when I query a filtered API. When I change my filters not one but as much as the previous offset calls are made.

Is it the correct way to use useSWRPages?

const query = buildQuery(filters)

const { pages, isEmpty, loadMore } = useSWRPages(
  `resorts-${query}`,
  ({ offset, withSWR }) => {
    const { data } = withSWR(
      useSWR(
        `/api/resorts?${query}&offset=` + (offset || 0),
        fetcher
      )
    )

    if (!data) {
      return <Loading />
    }

    return data.results?.map(resort => (
      <Card key={resort._id} resort={resort} />
    ))
  },
  ({ data }) => data?.nextOffset ?? null,
  [query]
)

@quietshu Thanks for the hard work on this package. Please any chance https://github.com/zeit/swr/pull/269 will be merged?

@Kerumen I would probably not put the query in the pageKey... If you don't, then you will want to reset the page on each query (which is why this thread exists).

Thanks.

Any progress on this?

Currently you can try

mutate('_swr_page_count_' + pageKey, 0)
mutate('_swr_page_offset_' + pageKey, 0)

as a temporary work around.

I will work on simplifying the useSWRPages API next week since it's tricky to get it right (conditional and dependent hooks) and we have several bugs/feature requests around it. πŸ™

Has this way worked for anyone?
I tried to mutate it that way but it seems to do nothing or at least not what was expected. I wanted the pageCount to be set back to 1 in order to show only the inital page and remove all the pages after {loadMore}

Hi,
I would like to check how do we use the below workaround - Got the same issue.

mutate('_swr_page_count_' + pageKey, 0)
mutate('_swr_page_offset_' + pageKey, 0)

@derekang That didn't work for me... Using this MR for now https://github.com/zeit/swr/pull/269

The workaround didn't work for me either.

It doesn't clear the pageCache, which is a local cache for useSWRPages.
It cannot be reset via mutate.

On Sun, May 17, 2020, 15:23 Dennis Lemm notifications@github.com wrote:

Currently you can try

mutate('_swr_page_count_' + pageKey, 0)
mutate('_swr_page_offset_' + pageKey, 0)

as a temporary work around.

I will work on simplifying the useSWRPages API next week since it's
tricky to get it right (conditional and dependent hooks) and we have
several bugs/feature requests around it. πŸ™

Has this way worked for anyone?
I tried to mutate it that way but it seems to do nothing or at least not
what was expected. I wanted the pageCount to be set back to 1 in order to
show only the inital page and remove all the pages after {loadMore}

β€”
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/zeit/swr/issues/189#issuecomment-629797894, or
unsubscribe
https://github.com/notifications/unsubscribe-auth/AAT6LTB3HW4ZVFRZ64PAEODRR7QOVANCNFSM4JYGAVGQ
.

Any progress on this?

I think I found a way.
You need to pass the filter key from top component to page component, which will be used as the page key and as the react key. As a result, when changing the key, the component will be rerendered and SWR will get cache for current pageKey.

For example:

// Main.tsx
...
const [currentTag, setCurrentTag] = useState('tag-1');
...

return (
  <>
    <Filter tag={currentTag} onTagChange={setCurrentTag} />
    <Content key={currentTag} tag={currentTag}  /> // it's important to set `key` here
  </>
)
// Content.tsx

const Content = ({ tag }) => {
  const { pages } = useSWRPages(
    pageKey: tag, // tag as pageKey here
    ...

  return <>{pages}</>
}
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import useSWR, { useSWRPages } from 'swr'
import { Box, Flex, Button, Heading, Input, Text } from '@chakra-ui/core'

import { SWRfetch } from 'lib/fetch'
import { config } from 'config'

import { ISubmissionList } from '../../@types/submission'

import { PageLayout } from 'components/Layout'
import { Td, Table, Th, Tr } from 'components/submissions/ListTable'

import { arrToObj } from 'utils/arrToObj'
import { insertQueryString } from 'utils/insertQueryString'

export default () => {
  const router = useRouter()
  const [username, setUsername] = useState('')
  const [task, setTask] = useState('')

  useEffect(() => {
    setUsername((router.query.username as string) || '')
    setTask((router.query.task as string) || '')
  }, [router.query])

  const { pages, isLoadingMore, isReachingEnd, loadMore } = useSWRPages(
    `submission-${username}-${task}`,
    ({ offset, withSWR }) => {
      const { data: submissions } = withSWR(
        useSWR(
          `${config.baseURL}/getSubmissions?offset=${
            offset || 0
          }&username=${username}&taskID=${task}`,
          SWRfetch
        )
      )

      if (!submissions) {
        return (
          <Tr>
            <td colSpan={7}>
              <Text textAlign={['start', 'center']} p={4}>
                Loading...
              </Text>
            </td>
          </Tr>
        )
      }

      const { results } = submissions

      return results.map((submission: ISubmissionList) => (
        <React.Fragment
          key={`submission-${username}-${task}-${submission.submissionID}`}
        >
          {submission ? (
            <Link href={`/submissions/${submission.submissionID}`}>
              <Tr>
                <Td>{submission.humanTimestamp}</Td>
                <Td>{submission.username}</Td>
                <Td>{submission.taskID}</Td>
                <Td>{submission.score}</Td>
                <Td>{arrToObj(config.languageData)[submission.language]}</Td>
                <Td>{submission.time}</Td>
                <Td>{submission.memory}</Td>
              </Tr>
            </Link>
          ) : (
            <Tr>
              <td colSpan={7}></td>
            </Tr>
          )}
        </React.Fragment>
      ))
    },
    ({ data: submissions }) => {
      return submissions.next
    },
    [username, task]
  )

  return (
    <PageLayout>
      <Flex justify="center" flexGrow={1} p={4}>
        <Box maxW="100%">
          <Heading>Submissions</Heading>
          <Flex mt={4} maxW="100%" direction={['column', 'row']}>
            <Input
              onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
                setUsername(event.target.value)
                insertQueryString('username', event.target.value)
              }}
              value={username}
              placeholder="Username"
              width="200px"
            />
            <Input
              onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
                setTask(event.target.value)
                insertQueryString('task', event.target.value)
              }}
              value={task}
              placeholder="Task"
              width="200px"
              ml={[0, 4]}
              mt={[4, 0]}
            />
          </Flex>
          <React.Fragment>
            <Box
              mt={4}
              boxShadow="var(--shadow-default)"
              borderRadius={4}
              width="1000px"
              maxW="100%"
              overflowX="scroll"
              borderBottom="1px solid #E2E8F0"
            >
              <Table>
                <thead>
                  <tr>
                    <Th>SUBMISSION TIME</Th>
                    <Th>USERNAME</Th>
                    <Th>TASK</Th>
                    <Th>POINTS</Th>
                    <Th>LANGUAGE</Th>
                    <Th>TIME</Th>
                    <Th>MEMORY</Th>
                  </tr>
                </thead>

                <tbody>{pages}</tbody>
              </Table>
            </Box>
            <Button
              onClick={loadMore}
              isLoading={isLoadingMore}
              isDisabled={isReachingEnd || isLoadingMore}
              mt={4}
              width="100%"
            >
              {isReachingEnd ? 'No more submission' : 'Load more'}
            </Button>
          </React.Fragment>
        </Box>
      </Flex>
    </PageLayout>
  )
}

I did it this way but it still don't work

@iam-medvedev I've tried it. It really works. Thank you.

@iammarkps Try to pass pageKey from parent component as prop. And it’s important to set react key to your component.

Ok, it kind of works for me now. Thanks to @iam-medvedev post. Although I think I did more kind of a strange workaround because I am not really using tags but a list of items, a load more and a reset button if reaching the end. I needed the reset button for showing the initial items of the list.

But after a second round of showing less it is not working anymore. I guess, because the changed pageKey already exists and therefore it is not updating the key a second time. So that might just a problem for my App.

I found a workaround for now with a random number at the end of the pageKey.

EDIT: I now use useEffect and a counter to generate increasing pageKey name to prevent dublicated pageKey and therefor no refreshing error.

EDIT: I now use useEffect and a counter to generate increasing pageKey name to prevent dublicated pageKey and therefor no refreshing error

This I do as well, but it has negative performance implications. What I see is:

  1. I perform a change
  2. useSWRPages on component is hooked
  3. Component will be rendered
  4. Component is unmounted, since it has a new key
  5. Component will be mounted
  6. useSWRPages will be hooked
  7. Component will be rendered

Are there any other/better solutions?

We have resorted to the following nuclear option (abbreviated for clarity):

import { cache } from 'swr'

export const onDelete = async ({ id }) => {
  await fetch(`/api/v1/items/${id}`, { method: 'DELETE' })

  cache
    .keys()
    .filter((key) => key.includes('/api/v1/items'))
    .forEach((key) => cache.delete(key))
}

Essentially, cache.keys() will give you all the current cache keys in useSWR, including the useSWRPages ones ; once you have that list you can delete the ones that need updating ; then you can unmount and remount the paginated component that needs updating with a key

Where would I ideally clear the cache?

Clear the whole cache? Only in tests probably, delete a key when you are not going to use it.

The example above could use mutate(key) too

My code looks like this:

  const { pages, isEmpty, isLoadingMore, isReachingEnd, loadMore } = useSWRPages(
    `paginated`,
    ({ offset, withSWR }) => {
      const { data, error } = withSWR(
        useSWR(
          [GET_DATA, offset || 0, someAtribute, anotherAttribute],
          fetcher,
          {
            initialData: offset ? undefined : intitalData,
            focusThrottleInterval: 60000,
          },
        )
      );
      if (!data?.paginated) return null;

      return data.paginated.edges.map((each) => (
        <div>{`${each}`}</div>
      ));
    },
    ({ data }) => {
      return data?.paginated.pageInfo.nextOffset;
    },
    [someAtribute, anotherAttribute]
  );

I delete the keys in cache when someAtribute or anotherAttribute is changed. Strange is when I change the value of one attribute useSWRPages runs with the previous value, a full render is performed and then useSWRPage runs again with the current values of someAtribute and anotherAttribute.

Another issue is that when I load a couple of pages with the same someAtribute and anotherAttribute and the I change a value, the same amount of pages is fetched for the new values. Since I do clear the cache the behaviour is strange.

Hi everyone! We are working on a new infinite loading API in #435 and the existing useSWRPages hook will be deprecated.

With the new API it should be just one line of code to clear the page: setPage(0). It will be very appreciated if you can give it a try!

Hi @shuding, thanks.

useSWRInfinite works much smoother now, but there are to many rerenders. This is my code now:

  const { data, error, mutate, page, setPage } = useSWRInfinite(
    (currentPageIndex, previousPageData) => {
      return [
        GET_DATA,
        currentPageIndex,
        someAtribute,
        anotherAttribute,
      ];
    },
    fetcher,
    {
      initialData,
    },
  )

Additionally I use useEffect in order to reset the page, when someAttribute or anotherAttribute changes:

  const didMountRef = useRef(false);
  useEffect(() => {
    if (didMountRef.current ) {
      setPage(1);
    } else {
      didMountRef.current = true;
    }
  }, [someAtribute, anotherAttribute]);

When data is fetched from the API data is rendered once. So let's say I set the value to A of someAttribute then data is fetched from the API and I see one render. Then I set the value to B of someAttribute again data is fetched from the API and rendered once. Now I set it to A again, data is not fetched from the API but from cache. In this case it renders twice, which is strange. Any idea why that happens?

The new pagination API has launched: https://swr.vercel.app/docs/pagination. Feel free to reopen if you are still encountering an issue!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Ephys picture Ephys  Β·  3Comments

sergiodxa picture sergiodxa  Β·  4Comments

zahraHaghi picture zahraHaghi  Β·  3Comments

tiagocorreiaalmeida picture tiagocorreiaalmeida  Β·  3Comments

nainardev picture nainardev  Β·  4Comments