useQuery returns undefined, But returns data on gql playground

C.K picture C.K · Jan 26, 2020 · Viewed 9.3k times · Source
"@apollo/react-hooks": "^3.1.3",
"apollo-client": "^2.6.8",

Apollo client return undefined on react app but return the data on gql playground, I don't understand why don't it works on client-side but works on graphql playground.

Schema

I have defined union for user query for error handling.

type Query {
  user(id: ID!): UserReturn!
}

union UserReturn = User | Error

type User {
  id: ID!
  username: String!
  email: String
  profileUrl: String
  createdAt: Date
  ads: [Doc!]!
}


type Error {
  message: String
  code: ID
}

Query resolver

 async user(_, { id }, { User }) {
    console.log('query - User')
    try {
      await delay(1000 * 3)
      const user = await User.findById(id).populate('userData')
      console.log(user)
      if (!user) return {
        __typename: 'Error',
        message: 'User not found.',
        code: id
      }

      const { _id: id, username, email, createdAt, userData: { profileUrl } } = user

      console.log(username)
      return {
        __typename: 'User',
        id,
        username,
        email,
        createdAt,
        profileUrl
      }
    } catch (err) {
      console.log(err)
      return {
        __typename: 'Error',
        message: 'Something went wrong while getting user.',
        code: LogBack(err, `query/user?id=${id}`, __filename)
      }
    }
  }

When querying on gql playground

on graphql playground, query works.

Screenshot from 2020-01-25 23-44-01_censored

On the client-side

 const { data } = useQuery(
    gql`query user($id: ID!) {
      user(id: $id) {
        __typename
        ... on User {
          id
          username
          email
          profileUrl
          createdAt
          # ads
        }
        ... on Error {
          message
          code
        }
      }
    }
    `,
    {
      variables: {
        id: userId
      }
    }
  );

  console.log(data) // undefined

useQuery runs but returns undefiend.

Answer

Jose A picture Jose A · Nov 18, 2020

Please, bear with me as this answer is long.

I ran into this issue as well. It seems the problem happens when using fragments (in this case, inline) and interfaces. I managed to solve it by passing the correct introspection data to Apollo's Heuristic Fragment Matcher (See Step 3).

Here's a detailed step by step guide on how to solve it:

1 - Verify Console Warnings.

Verify that there are warnings in your console (Here's an example that happened to me). These are the fields colliding with the default heuristic fragment matcher:

Firefox Dev Console with warnings

Reading the Apollo docs, I found out the following:

By default, Apollo Client's cache will use a heuristic fragment matcher, which assumes that a fragment matched if the result included all the fields in its selection set, and didn't match when any field was missing. This works in most cases, but it also means that Apollo Client cannot check the server response for you, and it cannot tell you when you're manually writing invalid data into the store using update, updateQuery, writeQuery, etc. Also, the heuristic fragment matcher will not work accurately when using fragments with unions or interfaces. Apollo Client will let you know this with a console warning (in development), if it attempts to use the default heuristic fragment matcher with unions/interfaces. The IntrospectionFragmentMatcher is the solution for working with unions/interfaces, and is explained in more detail below.

More info for v2 here: https://www.apollographql.com/docs/react/v2.6/data/fragments/#fragments-on-unions-and-interfaces

More info for v3 here: https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces

To fix this issue, we need to pass the IntrospectionResultData to the Apollo Client (See Step 3). But before that, we need to generate the file or data.

You have 3 options. Either do it manually or automatically (remote or local).

2 - Generate the introspection file

Pick one of the options below (all of them end up being the same). Read all of them before choosing one.

2.1 - Option A - Generating the file manually.

Use the following schema to adapt it to your own. Note, the following is TypeScript code. Remove the type if you're using plain JS.

Please see that in my case I had in my .gql file a union type of the following fashion:

   # GraphQL code omitted. 
  union PlanningResult = Planning | PlanningTechnical
// For Apollo V 2.x
export interface IntrospectionResultData {
  __schema: {
    types: {
      kind: string;
      name: string;
      possibleTypes: {
        name: string;
      }[];
    }[];
  };
}

const result: IntrospectionResultData = {
  __schema: {
    types: [
      {
        kind: 'UNION',
        name: 'PlanningResult',
        possibleTypes: [
          {
            name: 'Planning',
          },
          {
            name: 'PlanningTechnical',
          },
        ],
      },
    ],
  },
};
export default result;

// For Apollo V3:

      export interface PossibleTypesResultData {
        possibleTypes: {
          [key: string]: string[]
        }
      }
      const result: PossibleTypesResultData = {
  "possibleTypes": {
    "PlanningResult": [
      "Planning",
      "PlanningTechnical"
    ]
  }
};
      export default result;
    

Once you've done this, proceed to step 3.

2.2 - Option B - Automatic Remote Approach.

This is if you have your schema in a remote server and you'd like to fetch it. This is a script extracted directly from the Apollo Docs. For the automatic approach, you can fetch the schema directly as stated in the Apollo Docs:

// This is for V2 only, for V3 use the link down below (They're not the same!).

// For V2: https://www.apollographql.com/docs/react/v2.6/data/fragments/#fragments-on-unions-and-interfaces
// For V3 please, go to https://www.apollographql.com/docs/react/data/fragments/#generating-possibletypes-automatically

const fetch = require('node-fetch');
const fs = require('fs');

fetch(`${YOUR_API_HOST}/graphql`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    variables: {},
    query: `
      {
        __schema {
          types {
            kind
            name
            possibleTypes {
              name
            }
          }
        }
      }
    `,
  }),
})
  .then(result => result.json())
  .then(result => {
    // here we're filtering out any type information unrelated to unions or interfaces
    const filteredData = result.data.__schema.types.filter(
      type => type.possibleTypes !== null,
    );
    result.data.__schema.types = filteredData;
    fs.writeFile('./fragmentTypes.json', JSON.stringify(result.data), err => {
      if (err) {
        console.error('Error writing fragmentTypes file', err);
      } else {
        console.log('Fragment types successfully extracted!');
      }
    });
  });

This will generate a json file with the __schema and appropriate types. Once you've done this, proceed to step 3.

2.3 - Option C - Automatic Local Approach

The options above were difficult for me as I had my schema behind an auth wall. Fortunately, I did have direct local access to the .gql file and was able to generate the introspection file. Read on:

We use graphql-code-generator to generate the introspection file for us.

Go to your back-end code, or wherever your graphql.gql file lies, and do:

  1. Install GraphQL Code Generator:
yarn add graphql

yarn add -D @graphql-codegen/cli
  1. Run the initialization wizard:
yarn graphql-codegen init
  1. Fill in the details (adapt to your own) In my case, I selected:
  • Backend - API or server, Application built with React
  • Where is your schema? ./appsync/appSync.gql
  • Pick plugins: Fragment Matcher (Feel free to select other plugins as well... this is the important one!)
  • document: ./appsync/generated/introspection.ts (This is where you want the file to be outputted)

This will generate a codegen.yml that will include the plugins and the configuration for graphql-code-generator to run.

This is mine:

overwrite: true
schema: "./appsync/appSync.gql"
# documents: "./appsync/**/*.gql"
generates:
  ./appsync/generated/introspection.ts:
    plugins:
      # - "typescript"
      # - "typescript-operations"
      # - "typescript-resolvers"
      # - "typescript-react-apollo"
      - "fragment-matcher"
    config:
    # NOTE: Remember to specify the CORRECT Apollo Client Version
      apolloClientVersion: 2.6
  ./graphql.schema.json:
    plugins:
      - "introspection"

I've commented on the parts that are not critical for our mission.

Then (very important!) Run:

yarn install

Because the wizard adds packages to our package.json.

Then, generate the code:

yarn generate

This will output the introspection.ts file which needs to be included in Apollo to continue.

3 - Inject the introspection file to the ApolloClient

Now, in your front-end code, copy the introspection.ts file to your repo (if it's not already in there), and include it:

Note: I've renamed my file to fragmentTypes.ts and included it inside the apollo folder:

For V2:
import ApolloClient from 'apollo-client/ApolloClient';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { InMemoryCache } from 'apollo-cache-inmemory/lib/inMemoryCache';
// The file we just generated. If it's a .json file 
// remember to include the .json extension
import introspectionQueryResultData from './apollo/fragmentTypes';

const fragmentMatcher = new IntrospectionFragmentMatcher({
  introspectionQueryResultData,
});

export const globalClient = new ApolloClient({
  link,
  cache: new InMemoryCache({ fragmentMatcher }),
});

For V3:
import { InMemoryCache, ApolloClient } from '@apollo/client';
// In case you used graphql-code-generator
// import introspectionQueryResultData from './apollo/fragmentTypes';
// The file we just generated. If it's a .json file 
// remember to include the .json extension
import possibleTypes from './path/to/possibleTypes.json';

const cache = new InMemoryCache({
  possibleTypes,
});
const client = new ApolloClient({
  // ...other arguments...
  cache,
});

After this, your console warnings should go away and the queries and mutations should perform as normal.