0no-co / gql.tada

🪄 Magical GraphQL query engine for TypeScript
https://gql-tada.0no.co
MIT License
2.45k stars 36 forks source link

Fragment masking over React Client & Server Components #223

Closed hrougier closed 3 months ago

hrougier commented 3 months ago

Describe the bug

Using the following stack:

With fragment masking over react client & server components:

page.tsx (Server Component)

import { graphql } from "gql.tada";
import { apollo } from "@/graphql/server";
import Character, { CharacterFragment } from "./Character";

const query = graphql(
  `
    query Character {
      character(id: "1") {
        ...Character
      }
    }
  `,
  [CharacterFragment],
);

export default function Character() {
  const client = apollo();
  const {
    data: { character },
  } = await client.query({ query });

  return <Character character={character} />;
}

Character.tsx (client component)

"use client";

import { useCallback } from "react";
import { graphql, FragmentOf, readFragment } from "gql.tada";

export const CharacterFragment = graphql(`
  fragment Character on Character {
    name
  }
`);

export default function Character({
  character,
}: {
  character: FragmentOf<typeof CharacterFragment> | null;
}) {
  const { name } = readFragment(character);

  const onClick = useCallback(() => {
    console.log(`Hi, my name is ${name}`);
  }, [name]);

  return <button onClick={onClick}>{name}</button>;
}

The following error is thrown:

Error: Cannot access definitions.Symbol(Symbol.iterator) on the server. You cannot dot into a client module from a server component. You can only pass the imported name through.
    at Object.get (node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:253976)
    at graphql (webpack-internal:///(rsc)/./node_modules/gql.tada/dist/gql-tada.mjs:20:23)
    at eval (webpack-internal:///(rsc)/./src/app/page.tsx:14:64)
    at (rsc)/./src/app/page.tsx (.next/server/app/page.js:248:1)
    at Function.__webpack_require__ (.next/server/webpack-runtime.js:33:42)
    at async e5 (node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:396551)
    at async tv (node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:400209)
    at async tb (node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:400770)
    at async tb (node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:35:400901)
    at async tj (node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:36:2130)
    at async node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:36:2722

Is there some way to make this pattern work?

Reproduction

https://github.com/hrougier/gql.tada

gql.tada version

gql.tada v1.5.1

Validations

JoviDeCroock commented 3 months ago

This is because CharacterFragment is considered part of the client-module, hence when we try to merge the definitions (fragments) of said client export in the main operation we are dis-allowed. I don't really understand this limitation but it seems like one that Next chose conciously, not much we can do about that though.

kitten commented 3 months ago

I've been doing some investigating here, since I haven't really looked into this much yet. As it stands, Next.js doesn't seem to “split” client-modules and expose side-effect-less values.

Like, you can even add export const test = 'test'; in your client-modules, import that in your server file, and that would be replaced by a client reference.

So, you can't seemingly colocate fragments with client-components, while using them from a server component. You'd basically instead have to keep all components that consume fragments to either be universal (i.e. unmarked) or split your fragment out into a colocated file.

Just to clarify, yes, this does look very annoying. I hope there'll eventually be a solution to mark individual client-module exports as side-effectless and reusable i.e. shared.

kitten commented 3 months ago

Closing due to inactivity and since — seemingly — at least, we can't do much about this. My hope is that the React/Next teams support shared exports from client-component modules in the future to support colocated exports.