relay-hooks: Warning: RelayConnectionHandler: Unexpected after cursor `WyJuYXR1cmFsIiwxMF0=`, edges must be fetched from the end of the list (`WyJuYXR1cmFsIiwyMF0=`).

I got another strange bug I need to squash before I can carry on with other tasks.

On page http://my-app/my-posts there is a clickable element with onClick handler:

(event) => {
  event.preventDefault();
  history.push('/my-posts'); // same page as current value in browser address bar
}

On the same page there is a component with Load More button. When clicking on that button, it calls loadMore function returned by usePagination hook. It’s almost identical to the usePagination example.

If you press the Load More button without doing anything else, it loads another set of results from the graphql server without problems. It also works if you navigate to another page, and then back (doesn’t matter if using browser’s back button, or using another history.push call, it still works). But for some reason it fails after trying to navigate to the same page, you’re already at, for example you’re on page http://my-app/my-posts and call history.push(‘/my-posts’) and then press the Load More no new items are rendered and I get the following warning:

index.js:1 Warning: RelayConnectionHandler: Unexpected after cursor `WyJuYXR1cmFsIiwxMF0=`, edges must be fetched from the end of the list (`WyJuYXR1cmFsIiwyMF0=`).

I see that the graphql request and response are identical in both cases - when it works fine, and when it fails.

Here is the relevant stacktrace:

console.<computed> | @ | index.js:1
-- | -- | --
  | r | @ | backend.js:1
  | printWarning | @ | warning.js:30
  | push../node_modules/fbjs/lib/warning.js.warning | @ | warning.js:51
  | update | @ | RelayConnectionHandler.js:127
  | (anonymous) | @ | RelayPublishQueue.js:216
  | _getSourceFromPayload | @ | RelayPublishQueue.js:212
  | (anonymous) | @ | RelayPublishQueue.js:242
  | _commitData | @ | RelayPublishQueue.js:238
  | run | @ | RelayPublishQueue.js:178
  | _processResponse | @ | RelayModernQueryExecutor.js:315
  | _handleNext | @ | RelayModernQueryExecutor.js:263
  | (anonymous) | @ | RelayModernQueryExecutor.js:207
  | _schedule | @ | RelayModernQueryExecutor.js:178
  | _next | @ | RelayModernQueryExecutor.js:206
  | next | @ | RelayModernQueryExecutor.js:94
  | next | @ | RelayObservable.js:565
  | (anonymous) | @ | RelayNetworkLayer.js:55
  | Promise.then (async) |   |  
  | subscribe | @ | RelayNetworkLayer.js:54
  | (anonymous) | @ | RelayObservable.js:474
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | Executor | @ | RelayModernQueryExecutor.js:85
  | execute | @ | RelayModernQueryExecutor.js:43
  | (anonymous) | @ | RelayModernEnvironment.js:224
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | (anonymous) | @ | RelayObservable.js:233
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | (anonymous) | @ | fetchQueryInternal.js:114
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | (anonymous) | @ | RelayObservable.js:211
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | (anonymous) | @ | RelayObservable.js:369
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | (anonymous) | @ | RelayObservable.js:211
  | _subscribe | @ | RelayObservable.js:610
  | subscribe | @ | RelayObservable.js:296
  | push../node_modules/relay-hooks/lib/FragmentPagination.js.FragmentPagination._fetchPage | @ | FragmentPagination.js:346
  | push../node_modules/relay-hooks/lib/FragmentPagination.js.FragmentPagination.loadMore | @ | FragmentPagination.js:225
  | loadMore | @ | useOssFragment.js:94

Thanks for looking into it.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 51 (28 by maintainers)

Commits related to this issue

Most upvoted comments

It’s easy mistake to make, only 1 letter difference 😃

WyJuYXR1cmFsIiw_x_MF0 WyJuYXR1cmFsIiw_y_MF0

sorry 😃 they seemed to me the same 😄

WyJuYXR1cmFsIiwxMF0 WyJuYXR1cmFsIiwyMF0

can you share more code?

https://github.com/relay-tools/relay-hooks/releases/tag/v1.2.7

The final test did not pass … another change was needed.

https://github.com/relay-tools/relay-hooks/releases/tag/v1.2.7

Let me know if everything works, that I still have some time in the afternoon 😃

I was sure we would arrive at the solution: D your test case was fundamental (it is better that we do not say this to @sibelius ) 😃

Now release version 1.2.6

I close the issue after you’ve tried the new version ok? 😃

Yep, this fixes it for me.

Thank you! 🎉

Found the problem 😃 https://github.com/relay-tools/relay-hooks/blob/master/src/useOssFragment.tsx#L102 In the callback there is the reference to resolver and not to res. Then it used the old disposed resolver 😃

I have to do more tests 😃

But you can already try using this:

res.setCallback (function () {
             var newData = res.resolve();
             if (result.data! == newData) {
                 setResult ({resolver: res, data: newData});
             }
         });

Thank you so much for trying to get it resolved before you need to go.

Here is the repo as promised: https://github.com/jazblueful/loadmore-rerender

yours is good news;) from tomorrow to monday i’m on vacation, i can watch it but don’t try it … I hope that we can resolve It today;)

I wrote a unit test to reproduce the bug (should have done it way sooner):

import { render, cleanup, act, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
// @ts-ignore
import { createMockEnvironment, MockPayloadGenerator } from 'relay-test-utils';

import React, { FC } from 'react';
import { createOperationDescriptor, fetchQuery, getRequest, graphql, OperationDescriptor } from 'relay-runtime';
import { RelayEnvironmentProvider } from 'relay-hooks';

import { RouteDataContext, RouteData, RelayData, RelayDataContext } from '@util';
import { UserPrints } from '@prints/containers/UserPrints';

const query = graphql`
  query UserPrintsTestQuery($username: String!) {
    ...usePrintsByUsernameQueryConnection @arguments(username: $username)
  }
`;

let disposable;

const setup = (routeData?: RouteData) => {
  const environment = createMockEnvironment();

  if (!routeData) {
    // @ts-ignore
    routeData = {
      params: {
        username: 'reanna',
      },
    };
  }

  return {
    routeData,
    environment,
  };
};

afterEach(() => {
  cleanup();
});

test('Should render the component', async function() {
  const { routeData, environment } = setup();

  const variables = { username: 'reanna' };

  let fetchPromise = fetchQuery(environment, query, variables /*, { force: true }*/);

  expect(environment.mock.getAllOperations().length).toBe(1);

  act(() => {
    environment.mock.resolveMostRecentOperation((operation: OperationDescriptor) =>
      MockPayloadGenerator.generate(operation, {
        ID(_, generateId) {
          // Why we're doing this?
          // To make sure that we will generate a different set of ID
          // for elements on first page and the second page.
          return `first-page-id-${generateId()}`;
        },
        PageInfo() {
          return {
            hasNextPage: true,
          };
        },
      }),
    );
  });

  let relayData = (await fetchPromise) as RelayData;
  // fetchQuery bypasses the store and is stored in RecordSource directly.
  // Garbage collector would dispose the data in RecordSource,
  // unless we explicitly tell it to to retain it.
  // https://github.com/relay-tools/relay-hooks/issues/27
  let request = getRequest(query);
  let operation = createOperationDescriptor(request, variables);
  disposable = environment.retain(operation.root);

  const Component: FC<{}> = () => (
    <RelayEnvironmentProvider environment={environment}>
      <RouteDataContext.Provider value={routeData as RouteData}>
        <RelayDataContext.Provider value={relayData}>
          <UserPrints />
        </RelayDataContext.Provider>
      </RouteDataContext.Provider>
    </RelayEnvironmentProvider>
  );

  const { rerender, getByText } = render(<Component />);

  const loadMoreButton = getByText(/load more/i);
  expect(loadMoreButton).toBeInTheDocument();

  disposable.dispose();
  fetchPromise = fetchQuery(environment, query, variables /*, { force: true }*/);

  act(() => {
    environment.mock.resolveMostRecentOperation((operation: OperationDescriptor) =>
      MockPayloadGenerator.generate(operation, {
        ID(_, generateId) {
          return `first-page-id-${generateId()}`;
        },
        PageInfo() {
          return {
            hasNextPage: true,
          };
        },
      }),
    );
  });

  relayData = (await fetchPromise) as RelayData;

  rerender(<Component />); // commenting this line out, will make the last assertion pass

  fireEvent.click(loadMoreButton);

  act(() => {
    environment.mock.resolveMostRecentOperation((operation: OperationDescriptor) =>
      MockPayloadGenerator.generate(operation, {
        ID(_, generateId) {
          return `second-page-id-${generateId()}`;
        },
        PageInfo() {
          return {
            hasNextPage: false,
          };
        },
      }),
    );
  });

  expect(loadMoreButton).not.toBeInTheDocument();
});

The last assertion fails. It looks like it’s caused by rerendering the component after refetching the same data. I will extract the test and all the relevant code into a separate repo tomorrow for you to play with.

The situation is too strange, it seems that old data is recovered in the callback and data updated in the onNext function, as if there were two different stores, and from the information you gave me it all seems to work properly.

At the moment I can only ask you to recreate the error in a project where I can try or you have to investigate the situation more, adding console.log in the classes that I have indicated to you, and trying to understand what changes when you push (tried to avoid the push if you are already on the current page?).

Can you even try to change this? https://github.com/relay-tools/relay-hooks/blob/master/src/useOssFragment.tsx#L85-L87

in this way: const [result, setResult] = useState<ContainerResult>(newResolver());

You sent the diff of the data not of the resolver 😄

The store “seems” correct … Could you do the diff of the serialized object “resolver” here? https://github.com/relay-tools/relay-hooks/blob/80c7203ee21c4a8b9a186ef506812761628e4ba8/src/useOssFragment.tsx#L102

It might be useful to make the diff between the store content in these two scenarios at this point

When Load More works fine, result.data contains first 10 items and newData contains first 10 items + 10 newly loaded items (total of 20) so the if check passes and it triggers to rerender.

diff

When Load More doesn't work, both result.data and newData contain the same 10 items that were loaded before pressing the Load More button.

Could you summarize the operations you do when you detect the warning and when it works?

Moreover, you could debug in these two points in both cases.

https://github.com/relay-tools/relay-hooks/blob/master/src/FragmentPagination.ts#L471

https://github.com/relay-tools/relay-hooks/blob/master/src/useOssFragment.tsx#L102

request = environment.retain(operation.root); when unmount the component (or before re-invoking the fetchQuery): request.dispose()

maybe @sibelius can better point you to some articles.

Surely you should manage the dispose of the previous query correctly. So as to maintain a more consistent situation.

Then I can advise you to log what is rendered and what is published in the store (publish function of the store).

I try to respond to the warning: the first query is executed, updates the store but does not refresh the UI, the second is executed but recovered from the cache (network) and when it updates in the store the warning is returned because it finds the data from the first query.

The real question is why the UI in the first query is not updated.

What do you say?

1 thing I forgot to mention is that I need to press Load More button twice for the warning to be printed on the console.

On first click, graphql query is sent and correct response received, but the UI is not updated with data from the response and nothing is printed to the console.

On second click, no query is sent, the UI is not updated, but a warning is printed out to the console.