Skip to content

Instantly share code, notes, and snippets.

@captbaritone
Created September 11, 2023 18:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save captbaritone/42c183b03df984e0db4dcfd0fc24bfdf to your computer and use it in GitHub Desktop.
Save captbaritone/42c183b03df984e0db4dcfd0fc24bfdf to your computer and use it in GitHub Desktop.

This rule lints for non-colocated fragment spreads within queries or fragments. In other words, situations where a fragment is spread in module A, but the module (B) that defines that fragment is not imported by module A. It does not lint subscriptions or mutations. This catches:

  • The anti-pattern of spreading a fragment in a parent module, then passing that data down to a child module, or jamming it all in context. This defeats the purpose of Relay. From the Relay docs – "[Relay] allows components to specify what data they need and the Relay framework provides the data. This makes the data needs of inner components opaque and allows composition of those needs."
  • Instances where fragment spreads are unused, which results in overfetching. In addition to consuming more gCPU, it can also sometimes cause SEVs: D19057571

When the fragment is unused

The easiest way to tell if a fragment is unused is to remove the line containing the lint error, run js1 rebuild relay, then flow. If there are no errors, then the fragment was most likely unused. You should still test your functionality to see that it's working as expected.If you were able to remove it, congrats on improving gCPU (and other perf metrics) by reducing overfetching of data!

const FRAGMENT = graphql`
  fragment MyComponent_fragment on User {
    ...UnusedComponent_fragment
  }
;

The following diffs are examples on how to address this: D22390831

When the fragment is being passed to a child component

If you got Relay or Flow errors after attempting to remove the fragment, then it's very likely that you're passing that data down the tree, breaking a core concept in Relay—colocation of data.

Spreading fragments from components that aren't being rendered is an anti-pattern for a few reasons:

  • It couples two or more components that shouldn't have an implicit relationship, making these components harder to maintain as your app grows
  • It makes it impossible to tell if the fragment spread was left over (unused) after a refactor. This results in overfetching of data and unnecessary gCPU usage

Our strong recommendation is to have components specify the data they need. In the below example, this is an anti-pattern because Component B's data requirements are no longer opaque.

// ComponentA.react.js
function ComponentA(props) {
  const frag = useFragment(
    graphql`
      fragment ComponentA_foo on Type {
        ...ComponentC_foo # lint error - using fragment for ComponentC, but ComponentC isn't imported
      }
    `,
    props.frag
  );

  return <ComponentB frag={frag} />;
}

// ComponentB.react.js
function ComponentB(props) {
  return <ComponentC frag={props.frag} />;
}

// ComponentC.react.js
function ComponentC(props) {
  const frag = useFragment(
    graphql`
      fragment ComponentC_foo on Type {
        some_field
      }
    `,
    props.frag
  );

  return <div>{frag.some_field}</div>;
}

To address this, refactor Component C to fetch the data it needs. You'll need to update the intermediate components by amending, or adding a fragment to each intermediate component between ComponentA and ComponentC.

// ComponentA.react.js
function ComponentA(props) {
  const frag = useFragment(
    graphql`
      fragment ComponentA_foo on Type {
        ...ComponentB_foo # spread ComponentB's fragment as that component is the one being used
      }
    `,
    props.frag
  );

  return <ComponentB frag={frag} />;
}

// ComponentB.react.js
function ComponentB(props) {
  /** add a fragment to ComponentB which declares its dependencies **/
  const frag = useFragment(
    graphql`
      fragment ComponentB_foo on Type {
        ...ComponentC_foo
      }
    `,
    props.frag
  );
  return <ComponentC frag={frag} />;
}

// ComponentC.react.js
function ComponentC(props) {
  const frag = useFragment(
    graphql`
      fragment ComponentC_foo on Type {
        some_field
      }
    `,
    props.frag
  );

  return <div>{frag.some_field}</div>;
}

The following diffs are examples on how to address this: redacted

See also this workplace comment (LINK REDACTED) explaining the solution for a specific component.

Lint Suppression

In very rare edge cases such as this (LINK REDACTED), you might want to suppress this lint. Unfortunately eslint has native way to do inline lint suppressions in GraphQL (LINK REDACTED). As a workaround, support for only eslint-disable-next-line has been built directly into this rule and can be used be adding the following above your fragment

# eslint-disable-next-line fb-www/relay-must-colocate-fragment-spreads
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment