RevereCRE / relay-nextjs

⚡️ Relay integration for Next.js apps
https://reverecre.github.io/relay-nextjs/
MIT License
251 stars 30 forks source link

preloaded query is null when wrapping a nested component using withRelay #51

Closed 8byr0 closed 2 years ago

8byr0 commented 2 years ago

Hi! I'm struggling a bit with usePreloadedQuery in nested components. If implemented in root component (e.g. a page component) it works (first snippet). On the other hand if implemented deeper in the tree (second snippet), it fails with error

Cannot read property 'fetchKey' of null

(This error comes from react-relay/lib/relay-hooks/usePreloadedQuery.js (40:33) because the preloadedQuery object passed to it is null.)

TL;DR;

  1. Is it a good practice to have preloaded queries in nested components?
  2. Is withRelay HOC appropriate to be used for nested components?

If you want to reproduce easily, you can use this repo example, edit pages/index.tsx like this:

// rest of the file...

// export default withRelay(FilmList, FilmListQuery, {
const MyNestedComponent = withRelay(FilmList, FilmListQuery, {
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async () => {
    const { createServerEnvironment } = await import(
      'lib/server/relay_server_environment'
    );

    return createServerEnvironment();
  },
});

// Add this default export instead
export default function (props: any) {
  console.log('props', props);
  return <MyNestedComponent {...props} />;
}

Then run the server : image

Environment Node: 14 Next: 12 relay-nextjs: 0.7.0 react-relay: 13.1.1 react-compiler: 13.1.1

My implementation

Since this issue is related to my implementation, here are a few details, to follow on what I wrote above. (and also to help anyone coming from google that would be looking for snippets)

✅ Calling usePreloadedQuery in _app > Page Component (wired using withRelay)

const PAGE_QUERY = graphql`
  query all_items_Query {
    allItems {
      # this fragment is defined in AllItems component
      ...list_items
    }
  }
`;

function WrappedComponent({ preloadedQuery }: RelayProps<{}, all_items_Query>) {
  const query = usePreloadedQuery(PAGE_QUERY, preloadedQuery);
  return <AllItems relayRef={query.allItems} />;
}

// Page Component
export default withRelay(WrappedComponent, PAGE_QUERY, {
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async (ctx) => {
    const { createServerEnvironment } = await import(
      "lib/server/relay_server_environment"
    );

    const session = await getSession(ctx);

    const token = session?.user.apiToken;

    return createServerEnvironment(token);
  },
});

❌ calling usePreloadedQuery in _app > Page Component > Another Component (wired using withRelay)

const PAGE_QUERY = graphql`
  query all_items_Query {
    allItems {
      # this fragment is defined in AllItems component
      ...list_items
    }
  }
`;

function WrappedComponent({ preloadedQuery }: RelayProps<{}, all_items_Query>) {
  const query = usePreloadedQuery(PAGE_QUERY, preloadedQuery);
  return <AllItems relayRef={query.allItems} />;
}

const WrappedComponentWithRelay = withRelay(WrappedComponent, PAGE_QUERY, {
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async (ctx) => {
    const { createServerEnvironment } = await import(
      "lib/server/relay_server_environment"
    );

    const session = await getSession(ctx);

    const token = session?.user.apiToken;

    return createServerEnvironment(token);
  },
});

// Page component
export default function () {
  return <WrappedComponentWithRelay />;
}

_app and _document

My _app component is taken from the example in this repo. It implements the same behavior:

// src/pages/_app.tsx
const clientEnv = getClientEnvironment();
const initialPreloadedQuery = getInitialPreloadedQuery({
  createClientEnvironment: () => getClientEnvironment()!,
});

const MyApp: React.FC<MyAppProps> = ({
  Component,
  pageProps: { session, ...pageProps },
  emotionCache = clientSideEmotionCache,
}) => {
  const relayProps = getRelayProps(pageProps, initialPreloadedQuery);
  const env = relayProps.preloadedQuery?.environment ?? clientEnv!;

  return (
    <RelayEnvironmentProvider environment={env}>
      <Layout>
        <Component {...pageProps} {...relayProps} />
      </Layout>
    </RelayEnvironmentProvider>
  );
};

export default MyApp;
// src/pages/_document.tsx

interface DocumentProps {
  relayDocument: RelayDocument;
}

class MyDocument extends NextDocument<DocumentProps> {
  static async getInitialProps(ctx: DocumentContext) {
    const relayDocument = createRelayDocument();

    const renderPage = ctx.renderPage;
    ctx.renderPage = () =>
      renderPage({
        enhanceApp: (App) => relayDocument.enhance(App),
      });

    const initialProps = await NextDocument.getInitialProps(ctx);
    return {
      ...initialProps,
      relayDocument,
    };
  }

  render() {
    const { relayDocument } = this.props;

    return (
      <Html>
        <Head>
          <relayDocument.Script />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
FINDarkside commented 2 years ago
// Page component
export default function () {
  return <WrappedComponentWithRelay />;
}

That's not going to work because withRelay needs to be used to wrap the Page component.

If you want to pass stuff to deeper components you should make graphql fragments and then load the data with useFragment.

8byr0 commented 2 years ago

@FINDarkside Yes it's a mistake in my code which I did not make when testing out with the example project :

// Add this default export instead
export default function (props: any) {
  console.log('props', props);
  return <MyNestedComponent {...props} />;
}

I think I actually figured out the problem, withRelay can only be used around page components because it involves getServerSideProps which can only be used on root page components. This is why even forwarding all the props don't work as expected. (see https://stackoverflow.com/a/64138369/9568373). Am I right?

FINDarkside commented 2 years ago

You're right that it needs to be used with page components only. Small technical difference is that it doesn't use getServerSideProps though, but getInitialProps :) But it also works only with page components as you said.

8byr0 commented 2 years ago

Yes, my bad. And I actually feel ashamed cause it was written in the docs https://reverecre.github.io/relay-nextjs/docs/page-api#arguments :

component: A Next.js page component to recieve the preloaded query from relay-nextjs.

RTFM

TL;DR;

For anyone coming here with the same problem:

withRelay HOC can only be used with NextJS page components