Skip to content

useSuspenseQuery data is nullish despite the docs saying otherwise #527

@akin-fagbohun

Description

@akin-fagbohun

I have a question about this example within the docs under Fetching with Suspense.

There is a note that says plainly that data is guaranteed to be defined, however in my case, I'm having some issues. I've modified the snipped below slightly to illustrate the problem.

Image

Setup

For my particular case, I'm using PreloadQuery and colocated fragments in a server component that starts GET_DOG_QUERY on the server, meaning that client query should be "simulated", picking up the result from the one started on the server...

// on the server

// ... some server stuff

  return (
    <PreloadQuery
      query={GET_DOG_QUERY}
      variables={{
        id,
      }}
    >
      <Suspense fallback={<div>Loading doggo...</div>}>
        <Dog id="3" />
      </Suspense>
    </PreloadQuery>
  );

Nuanced example

"use client"

import { Suspense } from "react";
import { gql, TypedDocumentNode } from "@apollo/client";
import { useSuspenseQuery, useSuspenseFragment } from "@apollo/client/react";

interface Data {
  dog: {
    id: string;
    name: string;
  };
}

interface Variables {
  id: string;
}

interface DogProps {
  id: string;
}

// This query needs to be declared elsewhere as this is a client component
// and so can't live here and be imported into server component to be used in PreloadQuery
// I simply abstracted it nearby
const GET_DOG_QUERY: TypedDocumentNode<Data, Variables> = gql`
  query GetDog($id: String) {
    dog(id: $id) {
      # By default, an object's cache key is a combination of
      # its __typename and id fields, so we should always make
      # sure the id is in the response so our data can be
      # properly cached.
      id

      ...ColocatedFragmentWithNameAndBreed  
      # name
      # breed
    }
  }
`;

function Dog({ id }: DogProps) {

// This is now a simulated query as it's preloaded on the server
  const { data } = useSuspenseQuery(GET_DOG_QUERY, {
    variables: { id },
  });

// a few observations...
// 1. data.dog.name is masked because this component declares its data requirement with a fragment
// 2. we therefore have to unpack the masked fragment to get the data, which I'm doing as below in my project
// 3. data is not guaranteed as far as the ts compiler is concerned meaning that I have to assert it as non-null or type cast it

//  const dog = data.dog as Pick<NonNullable<GetDogQuery>, "dog">; - don't want to do this!

  const { data: fragmentData } = useSuspenseFragment({
    fragment: GET_DOG_QUERY,
    fragmentName: "ColocatedFragmentWithNameAndBreed",
    from: data.dog, // this isn't guaranteed - but is happy if I pass dog or assert non-null!
  });

  return <>Name: {fragmentData.name}</>;
}

Context

I should mention that I'm working in NextJS using the app router and @apollo/client-integration-nextjs. I use TypedDocumentNode and Codegen with masking configured correctly, so far as I can tell.

I'm really wanting to get on board with this new way of working with ApolloClient, but it was quite painful DX to get to even this point. The docs, which extensive feel like they have a lot of indirection. Is it a skill issue?

Are there examples in a repository somewhere? If so, maybe that needs improved discoverability as I've combed the docs many times, watched the talk, read the slides and am yet to see actual app-based patterns.

Anyway, what I have here works, but asserting non-null or casting feels a little unsavoury to me.

It's also one thing for me to find my way through this complexity, but trying to get an engineering team to adopt this will be a new challenge I'm not really looking forward to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions