0

I am getting an unexpected behaviour. That is, I have put a skip condition in the query and even when the skip condition is supposed to be true, the query is being run. I have made a minimal reproduction of the bug

App.jsx:

import { useState } from "react";
import "./App.css";
import { Test } from "../test";
import { useApolloClient } from "@apollo/client";

export default function App() {
  const [bool, setBool] = useState(false);
  const client = useApolloClient();
  return (
    <main>
      <button
        onClick={() => {
            if (bool) {
              client.resetStore();
            }
          setBool((bool) => {
            return !bool;
          });
        }}
      >
        Click me
      </button>
      {bool && <Test bool={bool} />}
    </main>
  );
}

test.jsx:

import { gql, useQuery } from "@apollo/client";

const testQuery = gql`
  query {
    countries {
      name
    }
  }
`;
export const Test = ({ bool }) => {
  const { data, loading, error } = useQuery(testQuery, {
    skip: !bool,
    variables:{holy:"shit"}
  });
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;
  return (
    <div>
      {data.countries.map((country) => (
        <div key={country.name}>{country.name}</div>
      ))}
    </div>
  );
};

I am using the countries API provided by Apollo GraphQL.

Here, when the button is clicked for the first time, the query is sent to server that makes sense. Then when it is clicked again (ie. making bool=false) the cache should be cleared and the query should not run as it is being skipped, But it does run (I can see it's running by looking at the network tab).

By doing conditional rendering of test I thought the query wouldn't run for sure as the component wouldn't even render but it was running again so I think the problem is that the query is running in between when the cache is cleared and ReactJS completely updates the state. I think I do not understand some concepts about states. How can I prevent the query from running when I don't want it to?

You can see it for yourself on Replit.

9
  • So when bool is truthy and Test component is mounted with {bool && <Test bool={bool} />} what do you think the value of skip: !bool is in the query hook? Commented Apr 9 at 21:44
  • @DrewReese skip will be false and the query wont be skipped but when the bool becomes false the skip will be true and the query should be skipped? I think this is whats supposed to happem. Commented Apr 9 at 21:59
  • OK, so when bool is falsey and Test is not mounted, do you think it's possible that useQuery can be called and the query can run? Commented Apr 9 at 22:08
  • @DrewReese No it should not be possible for useQuery to be called. But it is being called when the button is pressed. Like when the button is pressed twice there are two post request in network tabs. but since the bool is being flipped shouldn't there be only one post request ? Commented Apr 9 at 22:10
  • 1
    I'm not super familiar with Apollo, but maybe that client.resetStore(); is triggering another query before the bool state is toggled and the Test component unmounts. If you comment out that line do you see the double query request in the network tab? Do you see different behavior if you leave Test mounted so the skip value can actually be evaluated true and skip a query request? Commented Apr 9 at 22:20

2 Answers 2

1

Issue

The issues it seems is that the onClick handler in App is doing two things:

  1. Calls client.resetStore(); when bool is true
  2. Enqueues a bool state update to toggle the value to false

What I suspect is happening is that the query client is reset immediately and the React state update is only enqueued and is processed sometime later by React. While the query client is reset and Test is still mounted, another query request is fired off just before React processes the enqueued state update and triggers an App component rerender which will then result in Test unmounting.

Solution Suggestions

  • Delay the client.restore call by some small delay to be effected after the bool state update has had a chance to be processed and the App component tree rerendered.

    export default function App() {
      const [bool, setBool] = useState(false);
      const client = useApolloClient();
    
      return (
        <main>
          <button
            onClick={() => {
              setBool((bool) => {
                return !bool;
              });
              setTimeout(() => {
                if (bool) {
                  client.resetStore();
                }
              }, 100); // * Note
            }}
          >
            Click me
          </button>
          {bool && <Test bool={bool} />}
        </main>
      );
    }
    

    *Note: This appears to work with a delay as small as 4-5ms, but used a higher value just in case. This is something you can tweak to suit your needs best.

  • Use an AbortController and cancel any in-flight requests when Test component unmounts.

    export const Test = ({ bool }) => {
      const abortController = useRef(new AbortController());
    
      const { data, loading, error } = useQuery(testQuery, {
        skip: !bool,
        variables: { holy: "shit" },
        context: {
          fetchOptions: {
            signal: abortController.current.signal,
          }
        },
      });
    
      useEffect(() => {
        return () => {
          abortController.current.abort("Component unmounted");
        }
      }, []);
    
      if (loading) return <p>Loading...</p>;
      if (error) return <p>Error :(</p>;
      return (
        <div>
          {data.countries.map((country) => (
            <div key={country.name}>{country.name}</div>
          ))}
        </div>
      );
    };
    
Sign up to request clarification or add additional context in comments.

Comments

0

I think what is happening is when bool becomes falsy, the Test component instantly gets unmounted and the useQuery hook doesn't manage to register that the query is meant to be skipped. So for the useQuery hook, the value passed to skip is still false. Now, since Apollo refetches all active queries after calling resetStore() that query is also being refetched.

The Docs in ApolloGraphQL states that

It is important to remember that resetStore() will refetch any active queries. This means that any components that might be mounted will execute their queries again using your network interface. If you do not want to re-execute any queries then you should make sure to stop watching any active queries.

So technically the query should not be called at all since the component is unmounted, so it might be a bug.

I fixed the issue by not conditionally rendering the whole component rather just the ui of the component and let the query be skipped when bool is falsy.

App.jsx:

import { useEffect, useState } from "react";
import "./App.css";
import { Test } from "../test";
import { useApolloClient } from "@apollo/client";

export default function App() {
  const [bool, setBool] = useState(false);
  const client = useApolloClient();
  return (
    <main>
      <button
        onClick={() => {
          setBool((bool) => {
            client.resetStore()
            return !bool;
          });
        }}
      >
        Click me
      </button>
      <Test bool={bool} />
    </main>
  );
}

test.jsx:

import { gql, useQuery } from "@apollo/client";

const testQuery = gql`
  query {
    countries {
      name
    }
  }
`;
export const Test = ({ bool }) => {
  const { data, loading, error } = useQuery(testQuery, {
    skip: !bool,
    variables: { holy: "shit" },
  });
  if(!bool){
    return null
  }
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;
  return (
    <div>
      {data.countries.map((country) => (
        <div key={country.name}>{country.name}</div>
      ))}
    </div>
  );
};

2 Comments

Great insight — I ran into a similar issue. resetStore() will refetch all currently active queries, and if your component is in the process of unmounting, Apollo might still consider its query as active because useQuery hasn't finalized the cleanup. A good workaround (like you mentioned) is to keep the component mounted and just rely on skip: true to control the query. That way, Apollo won’t track the query as active at all when the condition is falsy. Might be worth filing an issue or feature request if Apollo can improve lifecycle handling around this edge case.
@TugrulYildirim I have filed an issue. github.com/apollographql/apollo-client/issues/12547

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.