RevereCRE / relay-nextjs

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

Possible to use `useRefetchableFragment` inside of page component? #18

Closed Blitz2145 closed 3 years ago

Blitz2145 commented 3 years ago

I'm getting an error and confusing stacktrace when trying to refetch a fragment inside of a withRelay wrapped page. Is this expected? I thought the suspense boundary on the page, would just show a loading and the refetch would succeed, maybe I've got the wrong mental model on what hooks can be used with this library.

rrdelaney commented 3 years ago

Yea that's definitely an expected use case and we do it all the time. Can you paste some example code? Without seeing anything my guess is that there's no suspense boundary around the part of the page using the refetchable fragment. relay-nextjs only adds a suspense boundary on client-side navigations to avoid rendering <Suspense> on the server (a hard error in React 17).

To get around this I recommend using next/dynamic with {ssr: false}, but still spreading the fragment so the data is fetched on the server. This incurs a minor perf penalty that will go away once React 18 is out 😄

rrdelaney commented 3 years ago

The next/dynamic + {ssr: false} approach is detailed more in the lazy-loaded query docs, but you'll have to adapt it for useRefetchableFragment.

Blitz2145 commented 3 years ago

Here's a code snippet with names redacted, general pattern is the same, rendering a list with refetchable fragment in the items.

import React from "react";
import { withRelay, RelayProps } from "relay-nextjs";
import {
  graphql,
  useFragment,
  usePreloadedQuery,
  useRefetchableFragment,
} from "react-relay/hooks";

import { getClientEnvironment } from "../lib/relay_client_environment";
import { styled } from "../stitches.config";

import { Shell } from "../components/Shell";
import { blahQuery } from "../__generated__/blahQuery.graphql";
import { blahLocationFragment_blah$key } from "../__generated__/blahLocationFragment_blah.graphql";

const BlahQuery = graphql`
  query blahQuery {
    blahs(first: 10) {
      edges {
        cursor
        node {
          id
          pk
          ...blahLocationFragment_blah
        }
      }
    }
  }
`;

const Card = styled("div", {
  display: "inline-grid",
  gap: "2ch",
  backgroundColor: "$surface2",
  padding: "2ch",
  borderRadius: "1ch",
  boxShadow: "0 2px 5px rgba(0, 0, 0, .2)",
});

function Blahs({
  preloadedQuery,
}: // eslint-disable-next-line @typescript-eslint/ban-types
RelayProps<{}, blahQuery>): JSX.Element {
  const query = usePreloadedQuery(BlahQuery, preloadedQuery);

  return (
    <Shell>
      <Card>
        <h2>Blahs</h2>
        <ul>
          {query.blahs.edges.map((edge) => {
            return (
              <li key={edge.node.id}>
                ID: ({edge.node.pk})
                <BlahLocation blah={edge.node} />
              </li>
            );
          })}
        </ul>
      </Card>
    </Shell>
  );
}

const blahLocationFragment = graphql`
  fragment blahLocationFragment_blah on Blah
  @refetchable(queryName: "BlahLocationRefetchQuery") {
    location {
      title
    }
  }
`;

interface BlahLocationProps {
  blah: blahLocationFragment_blah$key;
}

function BlahLocation(props: BlahLocationProps) {
  const [data, refetch] = useRefetchableFragment(
    blahLocationFragment,
    props.blah
  );

  return (
    <div>
      Hello: {data.location.title}
      <button
        onClick={() => {
          refetch({}, { fetchPolicy: "network-only" });
        }}
      >
        refetch
      </button>
    </div>
  );
}

function Loading() {
  return <div>Loading...</div>;
}

export default withRelay(Blahs, BlahQuery, {
  fallback: <Loading />,
  createClientEnvironment: () => getClientEnvironment()!,
  createServerEnvironment: async () => {
    const { createServerEnvironment } = await import(
      "../lib/server/relay_server_environment"
    );

    return createServerEnvironment();
  },
});
rrdelaney commented 3 years ago

Yep, you'll need to add a <Suspense> around usages of <BlahLocation>. Unfortunately this means it can't be server-side rendered (React throws a hard error), so you can either do an isMounted and conditionally render <BlahLocation> or put the component in its own file. I mentioned the separate file approach above, but the isMounted check looks like:

function BlahLocationContainer(props: BlahLocationProps) {
  const [isMounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);

  if (!isMounted) return null;
  return <Suspense fallback={<Spinner />}>
    <BlahLocation {...props} />
  </Suspense>;
}

and use BlahLocationContainer everywhere instead of BlahLocation.

Blitz2145 commented 3 years ago

@rrdelaney I see, thanks for the solutions, hopefully this will get easier in React 18. So withRelay does not provide a suspense boundary that would catch this particular suspend?

rrdelaney commented 3 years ago

Nope, even if it did it would do a full-page suspend which isn't great UX.

Blitz2145 commented 3 years ago

Makes sense now, thanks!