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

Update useLazyQuery with planned breaking changes #12367

Open
wants to merge 207 commits into
base: release-4.0
Choose a base branch
from

Conversation

jerelmiller
Copy link
Member

@jerelmiller jerelmiller commented Feb 12, 2025

Closes #5912
Closes #7484
Closes #9317
Closes #10787
Closes #12100
Partially addresses #12272

Adds the planned breaking changes to useLazyQuery planned for 4.0. A summary of the changes described in the changesets:


useLazyQuery no longer supports SSR environments and will now throw. If you need to run a query in an SSR environment, use useQuery instead.


The execute function returned from useLazyQuery now only supports the context and variables options. This means that passing options supported by the hook no longer override the hook value.

To change options, rerender the component with new options. These options will take effect with the next query execution.


The result resolved from the promise returned from the exeucte function in useLazyQuery is now an ApolloQueryResult type and no longer includes all the fields returned from the useLazyQuery hook tuple.

If you need access to the additional properties such as called, refetch, etc. not included in ApolloQueryResult, read them from the hook instead.


useLazyQuery will no longer rerender with the loading state when calling the execute function the first time unless the notifyOnNetworkStatusChange option is set to true.

If you prefer the behavior from 3.x, rerender the component with
notifyOnNetworkStatusChange set to false after the execute function is
called the first time.

function MyComponent() {
  const [notifyOnNetworkStatusChange, setNotifyOnNetworkStatusChange] = useState(true);
  const [execute] = useLazyQuery(query, { notifyOnNetworkStatusChange });

  async function runExecute() {
    await execute();

    // Set to false after the initial fetch to stop receiving notifications
    // about changes to the loading states.
    setNotifyOnNetworkStatusChange(false);
  }

  // ...
}

The reobserve option is no longer available in the result returned from useLazyQuery. This was considered an internal API and should not be used directly.


useLazyQuery no longer supports calling the execute function in render and will now throw. If you need to execute the query immediately, consider using useQuery instead.


The defaultOptions and initialFetchPolicy options are no longer supported with useLazyQuery.

If you use defaultOptions, pass those options directly to the hook instead. If you use initialFetchPolicy, use fetchPolicy instead.


useLazyQuery no longer supports variables in the hook options and therefore no longer performs variable merging. The execute function must now be called with variables instead.

function MyComponent() {
  const [execute] = useLazyQuery(query);

  function runExecute() {
    execute({ variables: { ... }});
  }
}

This change means the execute function returned from useLazyQuery is more type-safe. The execute function will require you to pass a variables option if the query type includes required variables.


useLazyQuery will now only execute the query when the execute function is called. Previously useLazyQuery would behave like useQuery after the first call to the execute function which means changes to options might perform network requests.

You can now safely rerender useLazyQuery with new options which will now take effect for the next query.


The promise returned when calling the execute function from useLazyQuery will now reject when using an errorPolicy of none when GraphQL errors are returned from the result.

Copy link

changeset-bot bot commented Feb 12, 2025

🦋 Changeset detected

Latest commit: 5dcb2df

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@apollo/client Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svc-apollo-docs
Copy link

svc-apollo-docs commented Feb 12, 2025

⚠️ Docs preview not attached to branch

The preview was not built because the PR's base branch release-4.0 is not in the list of sources.

An Apollo team member can comment one of the following commands to dictate which branch to attach the preview to:

  • !docs set-base-branch version-2.6
  • !docs set-base-branch main

Build ID: db532c96b5fb0512d8401339

Copy link

pkg-pr-new bot commented Feb 12, 2025

npm i https://pkg.pr.new/@apollo/client@12367

commit: 5dcb2df

Copy link
Contributor

github-actions bot commented Feb 12, 2025

size-limit report 📦

Path Size
dist/apollo-client.min.cjs 33.51 KB (+0.01% 🔺)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" 41.47 KB (-0.01% 🔽)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" (production) 38.77 KB (-0.01% 🔽)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" 36.2 KB (-0.01% 🔽)
import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" (production) 33.61 KB (-0.01% 🔽)
import { ApolloProvider } from "dist/react/index.js" 1.26 KB (0%)
import { ApolloProvider } from "dist/react/index.js" (production) 1.24 KB (0%)
import { useQuery } from "dist/react/index.js" 4.82 KB (-2.63% 🔽)
import { useQuery } from "dist/react/index.js" (production) 3.91 KB (-3.36% 🔽)
import { useLazyQuery } from "dist/react/index.js" 2.85 KB (-47.74% 🔽)
import { useLazyQuery } from "dist/react/index.js" (production) 2.66 KB (-41.37% 🔽)
import { useMutation } from "dist/react/index.js" 3.6 KB (0%)
import { useMutation } from "dist/react/index.js" (production) 2.82 KB (+0.04% 🔺)
import { useSubscription } from "dist/react/index.js" 4.44 KB (+0.03% 🔺)
import { useSubscription } from "dist/react/index.js" (production) 3.49 KB (-0.03% 🔽)
import { useSuspenseQuery } from "dist/react/index.js" 5.88 KB (+0.02% 🔺)
import { useSuspenseQuery } from "dist/react/index.js" (production) 4.54 KB (0%)
import { useBackgroundQuery } from "dist/react/index.js" 5.36 KB (-0.04% 🔽)
import { useBackgroundQuery } from "dist/react/index.js" (production) 4.02 KB (0%)
import { useLoadableQuery } from "dist/react/index.js" 5.44 KB (-0.08% 🔽)
import { useLoadableQuery } from "dist/react/index.js" (production) 4.09 KB (-0.03% 🔽)
import { useReadQuery } from "dist/react/index.js" 3.4 KB (-0.09% 🔽)
import { useReadQuery } from "dist/react/index.js" (production) 3.34 KB (-0.09% 🔽)
import { useFragment } from "dist/react/index.js" 2.01 KB (0%)
import { useFragment } from "dist/react/index.js" (production) 1.95 KB (0%)

Copy link

netlify bot commented Feb 12, 2025

Deploy Preview for apollo-client-docs ready!

Name Link
🔨 Latest commit 5dcb2df
🔍 Latest deploy log https://app.netlify.com/sites/apollo-client-docs/deploys/67afd4d05281a90008e7beb8
😎 Deploy Preview https://deploy-preview-12367--apollo-client-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@jerelmiller jerelmiller force-pushed the jerel/update-uselazyquery branch 3 times, most recently from 5f45a8c to 651926c Compare February 13, 2025 23:51
options?.fetchPolicy ??
client.defaultOptions.watchQuery?.fetchPolicy ??
"cache-first",
fetchPolicy: "standby",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wanted to change the default value for notifyOnNetworkStatusChange to true in 4.0. I'm going to reserve this for a separate PR and do this in core so that this change is consistent across the client.

const previousData = resultRef.current?.data;

if (previousData && !equal(previousData, result.data)) {
// eslint-disable-next-line react-compiler/react-compiler
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure why this eslint rule complains. I'm also writing resultRef.current below, but that one is just fine. Any ideas on what I'm doing unsafe here to trigger this lint rule?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe because you're not necessarily inside an uSES/useEffect here and updateResult could e.g. be called during render.
I bet it will stop complaining once you pass in previousDataRef as an argument.

}
}

function handleError(error: unknown) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eventually this will be moved out of here. We want to update ObservableQuery in 4.0 so that it never terminates on errors triggered by the link chain. Our React integration has sort of papered over this with the introduction of resubscribeAfterError to make it seem like the observable continues, but this is an internal API and not meant for end-use.

Once that is in place, we should be able to clean this up quite a bit, if not remove this entirely.

);

// TODO: Determine if this is still needed.
if (!hasOwnProperty.call(error, "graphQLErrors")) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added for legacy reasons, but this should disappear at some point.


const previousResult = resultRef.current;
if (!previousResult || !equal(error, previousResult.error)) {
updateResult(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be moved to core in a future PR.

@jerelmiller jerelmiller changed the title [WIP] Changes to useLazyQuery Update useLazyQuery with planned breaking changes Feb 14, 2025
@jerelmiller jerelmiller marked this pull request as ready for review February 14, 2025 01:42
@jerelmiller jerelmiller requested a review from phryneas February 14, 2025 01:42
});

// TODO: Should we delete this? This is covered by the first test
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking yes, but figured I'd ask first, especially since this might make this diff even more difficult to see.

called: false,
loading: false,
networkStatus: NetworkStatus.ready,
previousData: undefined,
variables: { id: 1 },
variables: {},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value should be undefined instead of an empty object since this the variables type is TVariables | undefined. This will be addressed in a separate PR so that useQuery can benefit as well.

variables: {},
});
}
expect(result).toEqualApolloQueryResult({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the major changes in this PR. The return type of the execute function is now ApolloQueryResult rather than QueryResult (or LazyQueryResult as this PR adds) which means many of the properties returned on this type are now removed. We used to add everything (refetch, fetchMore, etc.) to the resolved value, but this felt excessive. This forces you to use those from the hook instead.


setTimeout(() => execute());

{
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another big change in this PR is that loading states are no longer rendered when calling execute for the first time. Instead you must set notifyOnNetworkStatusChange in order to receive updates to ANY loading state.

NOTE: A separate PR will be opened in core to change the default of notifyOnNetworkStatusChange to true which means this will likely be added back, but wanted to point this out as this better aligns the core API and this hook behavior.

});

it("changing queries", async () => {
it("applies changed query to next refetch after execute", async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very similar to the "changing queries" test with the difference that we call refetch instead of the execute function which checks to make sure the query is applied when rerendering this component rather than the next time execute is called.

Comment on lines +974 to +975
// TODO: Determine if this hook makes sense for polling or if that should be
// reserved for useQuery. At the very least, we need to figure out if you can
// start polling a query before it has been executed
it.skip("should allow for the query to start with polling", async () => {
Copy link
Member Author

@jerelmiller jerelmiller Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drawing attention to this. Lenz and I already discussed this, but wanted to revisit this conversation after we've had a chance to sit on this a bit longer.

I'm still personally torn on how polling should work with useLazyQuery, mostly due to the dev experience either direction. The declarative API is nice (pollInterval), but is it confusing that it won't take effect until after you've called the execute function? The imperative API is nice for that reason as its more explicit when it will begin polling, and we can throw an error if you try and use it too early, but it does feel more in-the-way.

I'm also torn about allowing both of them. Its always been unclear to me whether the pollInterval value or the startPolling function have higher priority. For example, what happens if you stopPolling(), then change the pollInterval value on a future render? Should it start polling because its non-zero, or should that be reserved for startPolling?

Would love to toss around some more ideas. Regardless, we should probably address both of these together since any decision here might affect useQuery as well.

@jerelmiller jerelmiller force-pushed the jerel/update-uselazyquery branch from adcb8e4 to 861e23c Compare February 14, 2025 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment