Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useQuery with changed variable on Apollo Cache doesn't work #6734

Closed
hatchli opened this issue Jul 29, 2020 · 4 comments
Closed

useQuery with changed variable on Apollo Cache doesn't work #6734

hatchli opened this issue Jul 29, 2020 · 4 comments

Comments

@hatchli
Copy link

hatchli commented Jul 29, 2020

Intended outcome:
Populated Apollo Cache could be filtered with useQuery's refetch or useLazyQuery.

Actual outcome:
Only useLazyQuery and useQuery's refetch with identitical variables will return cache. Otherwise, despite all the data being there, nothing will be returned.
How to reproduce the issue:

This basic useQuery breaks the website:

  const {
    data,
    error,
    loading,
    refetch,
    fetchMore,
  } = useQuery(GET_PRODUCTS, {
    variables: {
      where: {
        type: type?.type,
        live: { equals: true },
      },
    },
  });

MutationsQueries.js

export const GET_PRODUCTS = gql`
  query(
    $where: ProductWhereInput
    $orderBy: ProductOrderByInput
    $take: Int
    $skip: Int
    $cursor: ProductWhereUniqueInput
  ) {
    products(
      where: $where
      take: $take
      skip: $skip
      orderBy: $orderBy
      cursor: $cursor
    ) {
      id
      title
      type
      slug
      stock
      live
      headline
      subHeadline
      extendedDescription
      features {
        id
        feature
      }
      categories {
        id
        title
        slug
      }
      SubCategory {
        id
        slug
        title
      }
      unit
      image
      description
      price
      salePrice
      discountInPercent
      url
      featured
      gallery {
        id
        url
      }
    }
  }
`;

_app

import React from "react";
import Page from "../landing/Page";
import { ApolloProvider } from "@apollo/client";;
import { useApollo } from "../lib/apolloClient";

export function reportWebVitals(metric) {
  // These metrics can be sent to any analytics service
  console.log(metric);
}

const myApp = ({ Component, pageProps, appProps }) => {
  const apolloClient = useApollo(pageProps.initialApolloState);

  return (
    <ApolloProvider client={client}>
      <Page>
        <Component {...pageProps} />
      </Page>
    </ApolloProvider>
  );
};


export default myApp;

apolloClient (for initializing Apollo Store via Next.js' getStaticProps)

import { useMemo } from "react";
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/link-context";
import { concatPagination } from "@apollo/client/utilities";
let apolloClient;
const dev = "http://localhost:4000/api";
const prod = process.env.BACKEND_URL_MYO;
const isBrowser = typeof window !== "undefined";
const cache = new InMemoryCache();
const httpLink = createHttpLink({
  // uri: process.env.BACKEND_URL_MYO,
  uri: dev,
  onError: ({ networkError, graphQLErrors }) => {
    console.log("graphQLErrors wtihin httplink", graphQLErrors);
    console.log("networkError within httplink", networkError);
  },
  credentials: "include",
  fetch,
});
const authLink = setContext((_, { headers }) => {
  const token = isBrowser ? windowlocalStorage.getItem("token") : undefined;
  // return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === "undefined",
    link: authLink.concat(httpLink),
    disableOffline: true,
    cache,
  });
}

export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient();

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    _apolloClient.cache.restore(initialState);
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === "undefined") return _apolloClient;
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient;

  return _apolloClient;
}

export function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState]);
  return store;
}

Schema

model Product {
  id                  Int          @default(autoincrement()) @id
  slug                String       @unique
  title               String
  headline            String?
  subHeadline         String?
  extendedDescription String?
  features            Features[]
  type                ProductType
  categories          Category[]   @relation(references: [id])
  unit                String
  image               String
  gallery             Gallery[]
  description         String
  price               Float
  stock               Int?
  salePrice           Float
  discountInPercent   Float
  createdBy           User         @relation(fields: [createdById], references: [user_id])
  createdById         Int
  Meta                Meta?        @relation(fields: [metaId], references: [id])
  metaId              Int?
  createdAt           DateTime     @default(now())
  SubCategory         SubCategory? @relation(fields: [subCategoryId], references: [id])
  subCategoryId       Int?
  Customer            Customer?    @relation(fields: [customerId], references: [id])
  customerId          Int?
  url                 String?
  featured            Boolean      @default(false)
  live                Boolean      @default(false)
}

Versions
System:
OS: Windows 10 10.0.18362
Binaries:
Node: 10.16.0 - C:\Program Files\nodejs\node.EXE
Yarn: 1.22.4 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 6.9.0 - C:\Program Files\nodejs\npm.CMD
Browsers:
Edge: Spartan (44.18362.449.0)
npmPackages:
@apollo/client: ^3.0.2 => 3.0.2
@apollo/link-context: ^2.0.0-beta.3 => 2.0.0-beta.3
@apollo/link-ws: ^2.0.0-beta.3 => 2.0.0-beta.3
apollo-cache-persist-dev: ^0.2.1 => 0.2.1
apollo-link: ^1.2.13 => 1.2.14
apollo-link-context: ^1.0.19 => 1.0.20

@benjamn
Copy link
Member

benjamn commented Jul 29, 2020

You may be right that all the data is in the cache already, but the cache can't guess which variables are relevant for reading this particular query, so it's unsafe to return data when the variables don't match exactly.

Fortunately, in AC3, you can define a custom read and merge functions for the Query.products field, which can process/examine the existing cache data and the arguments received by the field, to support flexible ways of querying the existing data:

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        products: {
          // Prevent the cache from using the arguments to store separate values for this field:
          keyArgs: false,
          // Define how to use args.{where,take,...} to return flexible views of the existing data:
          read(existing, { args }) {
            // Note: existing is whatever merge returns, and may be undefined if no data has been written yet.
            return readWithArgs(existing, args);
          },
          // Define how new data is combined with existing data to support reading:
          merge(existing = emptyData(), incoming, { args }) {
            return mergeWithArgs(existing, incoming, args);
          },
        },
      },
    },
  },      
});

To be clear, readWithArgs and mergeWithArgs are just placeholders for your custom logic. As long as read and merge cooperate, you can do anything you want with this system.

See the documentation for further explanation, this recent comment for a related discussion of keyArgs, and these helper functions for some examples of generating reusable field policies. We don't have any helpers that exactly match your use case, but offsetLimitPagination is probably the closest starting point.

I know that's a lot to digest, but we're happy to answer any questions you have!

@hatchli
Copy link
Author

hatchli commented Jul 29, 2020

Ok, thank you @benjamn.

You're right, it's a lot to digest. I was really hoping I was just missing something obvious, and that an elegant solution already existed for automatically interacting with cache. Not to worry though, I will study this.


EDIT:
So, the curiousness below still stands. But I think I get the gist - I need to manually sort. But the problem I am facing now is, how do I sort when I am getting back __ref: 'Product:2' for example? I've tried using readField and toReference from the third parameter of read, but I either get undefined, or the exact same thing back...

          read(existing, { args, readField, toReference }) {
            console.log("existing", existing);
            if (existing && existing.length > 0) {
              const reference = existing.map((a) =>
                readField("Product", a.__ref)
              );
              console.log(reference);
            }

I assume that what I need to do is something like this (if I were trying to filter based on the property type on Product:

          read(existing, { args, readField, toReference }) {
            console.log("existing", existing);
            if (existing && existing.length > 0) {
              const reference = existing.map((a) =>
                readField("Product", a.__ref)
              );
              return reference.filter(a=>a === args.type);
            }

One curious thing, when I attempt to console log the parameters, the read parameters return something (I have to study what _Ref refers to, surely the original item in the cache), but the merge parameter doesn't. It would seem merge is never reached...

  typePolicies: {
    Query: {
      fields: {
        products: {
          // Prevent the cache from using the arguments to store separate values for this field:
          keyArgs: false,
          // Define how to use args.{where,take,...} to return flexible views of the existing data:
          read(existing, { args }) {
            console.log("existing", existing);
            console.log("args", args);
            // Note: existing is whatever merge returns, and may be undefined if no data has been written yet.
            return existing;
          },
          // Define how new data is combined with existing data to support reading:
          merge(existing, incoming, { args }) {
            console.log("existing", existing);
            console.log("incoming", incoming);
            console.log("args", args);
            return incoming;
          },
        },
      },
    },
  },
});

@benjamn
Copy link
Member

benjamn commented Jul 29, 2020

Ok so, the readField helper takes a field name (string) as the first argument, and a Reference (the things that look like { __ref: <ID string> }) as the second argument, so maybe try readField("id", a) to get the id field from the a object?

By the way, the readField function will also work if the second argument is a normal object (not a Reference), so you should be able to use it regardless of whether you have references or regular objects.

@hatchli
Copy link
Author

hatchli commented Jul 30, 2020

You're the man @benjamn ! You gave me just enough info and context to get me on the right track and learn! Successful implementation! Definitely a bit more work than just calling useQuery, but not too much to make the use of cached data a waste of resources. Thank you again.

Once I have finished writing the custom logic, I'll share here for anyone in the future who's curious. The other Issues here helped immeasurably.

@hatchli hatchli closed this as completed Jul 30, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants