Skip to content

Conversation

github-actions[bot]
Copy link
Contributor

@github-actions github-actions bot commented Jul 8, 2025

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to master, this PR will be updated.

Releases

[email protected]

Major Changes

  • #2372
    44ed9a5
    Thanks @jerelmiller! - Namespaced types

    Before:

    import type {
      MutationOptionsAlone,
      QueryOptionsAlone,
      SubscriptionOptionsAlone,
      WatchQueryOptions,
      WatchQueryOptionsAlone,
    } from 'apollo-angular';
    import type { BatchOptions, Options } from 'apollo-angular/http';
    
    type AllTypes =
      | Options
      | BatchOptions
      | MutationOptionsAlone
      | QueryOptionsAlone
      | SubscriptionOptionsAlone
      | WatchQueryOptions
      | WatchQueryOptionsAlone;

    After:

    import type { Apollo, Mutation, Query, Subscription } from 'apollo-angular';
    import type { HttpBatchLink, HttpLink } from 'apollo-angular/http';
    
    type AllTypes =
      | HttpLink.Options
      | HttpBatchLink.Options
      | Mutation.MutateOptions
      | Query.FetchOptions
      | Subscription.SubscribeOptions
      | Apollo.WatchQueryOptions
      | Query.WatchOptions;
  • #2372
    bdc93df
    Thanks @jerelmiller! - httpHeaders is a class

    Migrate your code like so:

    - const link = httpHeaders();
    + const link = new HttpHeadersLink();
  • #2372
    8c0b7f0
    Thanks @jerelmiller! - Move useZone option into subscription
    options

    - const obs = apollo.subscribe(options, { useZone: false });
    + const obs = apollo.subscribe({ ...options, useZone: false });
  • #2372
    b9c62a5
    Thanks @jerelmiller! - Combined parameters of Query,
    Mutation and Subscription classes generated via codegen

    Migrate your code like so:

    class MyComponent {
      myQuery = inject(MyQuery);
      myMutation = inject(MyMutation);
      mySubscription = inject(MySubscription);
    
      constructor() {
    -    myQuery.watch({ myVariable: 'foo' }, { fetchPolicy: 'cache-and-network' });
    +    myQuery.watch({ variables: { myVariable: 'foo' }, fetchPolicy: 'cache-and-network' })
    
    -    myMutation.mutate({ myVariable: 'foo' }, { errorPolicy: 'ignore' });
    +    myMutation.mutate({ variables: { myVariable: 'foo' }, errorPolicy: 'ignore' });
    
    -    mySubscription.subscribe({ myVariable: 'foo' }, { fetchPolicy: 'network-only' });
    +    mySubscription.subscribe({ variables: { myVariable: 'foo' }, fetchPolicy: 'network-only' });
      }
    }

Patch Changes

@github-actions github-actions bot force-pushed the changeset-release/master branch 2 times, most recently from 421b4e4 to de947bc Compare July 9, 2025 07:52
@github-actions github-actions bot force-pushed the changeset-release/master branch 2 times, most recently from 50769ba to fd6c62a Compare August 26, 2025 08:55
@github-actions github-actions bot force-pushed the changeset-release/master branch from fd6c62a to 12ce8e9 Compare September 25, 2025 10:20
@PowerKiKi
Copy link
Collaborator

@diesieben07, @KillerCodeMonkey, @muuvmuuv, @KeithGillette, @JosephHalter, @tomachristian and @reicheltp, you all expressed interest in support for Apollo Client 4.0 by liking the PRs. It would be super useful if you could test the alpha version of apollo-angular and report your findings here.

Keep in mind that it is a major version both for Apollo Client and for Apollo Angular. And as such as lot of things might change for both libraries. The upgrade might be a bit rough. But that's precisely why your tests would be a big help ❤️

You can try the alpha with something like:

yarn add apollo-angular@12.0.0-alpha-20250922033952-84d3fb552850c03e10b2a2db9c6c8ac78f124906

or

npm i apollo-angular@12.0.0-alpha-20250922033952-84d3fb552850c03e10b2a2db9c6c8ac78f124906

@JosephHalter
Copy link

JosephHalter commented Sep 25, 2025

Tried to update a large app, the fact that useInitialLoading: false isn't available anymore requires a huge amount of changes but since it was deprecated it was to be expected. Beyond that, the most problematic is that all returned objects seem to be of type DeepPartialObject even if returnPartialData is false, and all the calls are made with fetchPolicy: 'no-cache', nextFetchPolicy: 'no-cache'.

Here's the options to provideApollo:

        defaultOptions: {
          watchQuery: { fetchPolicy: 'no-cache', nextFetchPolicy: 'no-cache', returnPartialData: false },
          query: { fetchPolicy: 'no-cache', returnPartialData: false },
          mutate: { fetchPolicy: 'no-cache', returnPartialData: false },
        },

and we never override the fetchPolicy anywhere. I'm not sure how to avoid ending up with everything being ExpectedType[] | (DeepPartialObject<ExpectedType> | undefined)[] instead of ExpectedType[] like before. In our project, we've this particular error 4012 times so we need a global way to say that we only want full objects. Any help appreciated.

@JosephHalter
Copy link

Apparently checking on result.dataState==='complete' before accessing result.data is the only way to get the right type, I've a lot of checks to add...

@jerelmiller
Copy link
Contributor

jerelmiller commented Sep 25, 2025

For those of you looking to upgrade to Apollo Client 4, I'd also recommend looking at the migration guide which details what changes you need in your app. We included a codemod that will update your imports (including the updates to type names) which I'd commend as well 🙂.

@JosephHalter in regards to DeepPartial, it's returned now because its incredibly difficult to check for the returnPartialData since it can be changed after the creation of the ObservableQuery. For example:

// Initialize ObservableQuery without partial results
const obsevable = client.watchQuery({
  query,
  returnPartialData: false
});

// Now allow partial results
observable.reobserve({ returnPartialData: true })

In this case, there is no way to accurately detect when you've switched over/away from partial results, hence why DeepPartial is included in the result.

As you've already figure out, dataState is the right way to type narrow the result so that you can avoid that DeepPartial. You can learn more about the TypeScript changes, including dataState in the TypeScript docs.

Hope that helps!

@jerelmiller
Copy link
Contributor

jerelmiller commented Sep 25, 2025

re: useInitialLoading

I also wanted to point out, this was removed because Apollo Client core made two changes that rendered this option unnecessary:

  • notifyOnNetworkStatusChange now defaults to true so you'll get loading states out-of-the-box
  • ObservableQuery now emits an initial loading state when notifyOnNetworkStatusChange is true and the result can't be fulfilled from the cache, rather than waiting for the initial fetch to complete before emitting anything.

So if you need to control the loading state, use the notifyOnNetworkStatusChange option instead 🙂

@JosephHalter
Copy link

JosephHalter commented Sep 26, 2025

I was able to make all tests pass using the alpha version (and we're talking about a large app from a Fortune 500 company). There are many gotchas, for example the case was that previously handled with error.networkError?.status===0 isn't covered by any of:

So I had to use isNetworkError from https://www.npmjs.com/package/is-network-error but for me [email protected] is ready for release.

@PowerKiKi
Copy link
Collaborator

Apparently checking on result.dataState==='complete' before accessing result.data is the only way to get the right type, I've a lot of checks to add...

@JosephHalter, with jeremiller we talked about adding some sort of helper. Possibly something that could be used like so:

.valueChanges.pipe(
    filterCompleteData(),
    map(result => result.data.film), // result.data.film guaranteed to exists
);

In your experience is that something that could be useful if provided by apollo-angular or apollo-client ?

I was also wondering if a slighlty more flexible API might be even more useful, something like

// Same as before
.valueChanges.pipe(
    only('complete'),
    map(result => result.data.film), // result.data.film guaranteed to exists
);
// different datastate
.valueChanges.pipe(
    only('partial'),
    map(result => result.data?.film), // might or might not exist
);
// Or even combined state...
.valueChanges.pipe(
    only('partial', 'streaming'),
    map(result => result.data?.film), // might or might not exist
);

Any thought about that ? how did you solve it in your codebase ?

@JosephHalter
Copy link

JosephHalter commented Sep 26, 2025

@PowerKiKi I would have used the helper if it was there, but I'm not sure it's necessary. Once I knew that I could get the correct type by checking on dataState, I didn't really need it anymore.

If there was a global setting to disable partial results everywhere I would have used it, but if I need to go to each place and use the helper, I might just as well check on dataState directly.

@jerelmiller
Copy link
Contributor

jerelmiller commented Sep 26, 2025

the case was that previously handled with error.networkError?.status===0 isn't covered by any of:

Apollo Client 4 doesn't wrap "external" errors anymore (i.e. non Apollo-thrown errors), so unless that error originated from Apollo Client itself, you just use the error instance directly. Those error classes aren't useful here if the thrown error isn't one of those error instances.

In your case, you might just need to change to use error.status === 0 when that error is a generic error and if that property existed before. That said isNetworkError probably works great here as well 🙂

@mark7-bell
Copy link
Contributor

Our app is working with the alpha.

@KeithGillette
Copy link
Contributor

It looks like we have a fair bit of manual refactoring working to get our application working with Apollo Client 4/Apollo Angular 12. A few initial questions:

  • Where is the ExtraSubscriptionOptions type?
  • We had been using the generic ApolloCache to type the results in our mutation update methods. Is there a recommended way to type those results now?
  • Has the result() method been removed from QueryRef?

@PowerKiKi
Copy link
Collaborator

  • Where is the ExtraSubscriptionOptions type?

ExtraSubscriptionOptions was dropped entirely, because its only member useZone is now part of Apollo.SubscribeOptions:

- const obs = apollo.subscribe(options, { useZone: false });
+ const obs = apollo.subscribe({ ...options, useZone: false });
  • We had been using the generic ApolloCache to type the results in our mutation update methods. Is there a recommended way to type those results now?

That change comes from apollo-client. Their migration doc does not seem to cover your use-case. And I cannot recommend anything either. Maybe @jerelmiller could share some thoughts on that ?

  • Has the result() method been removed from QueryRef?

Yes it was removed from apollo-client (and thus from apollo-angular). They recommend:

If you use this method and need similar functionality, use the firstValueFrom helper in RxJS.

import { firstValueFrom, from } from "rxjs";

// The `from` is necessary to turn `observableQuery` into an RxJS observable
const result = await firstValueFrom(from(observableQuery));

@KeithGillette
Copy link
Contributor

Thanks for the response, @PowerKiKi.

  • We had been using the generic ApolloCache to type the results in our mutation update methods. Is there a recommended way to type those results now?

That change comes from apollo-client. Their migration doc does not seem to cover your use-case. And I cannot recommend anything either. Maybe @jerelmiller could share some thoughts on that ?

Now that I've cleared out some more outdated typings, the Mutation update method arguments seem to be getting typed correctly, so this may not be an issue. Not 100% sure yet since we're in the process of refactoring.

  • Has the result() method been removed from QueryRef?

Yes it was removed from apollo-client (and thus from apollo-angular). They recommend:

If you use this method and need similar functionality, use the firstValueFrom helper in RxJS.

import { firstValueFrom, from } from "rxjs";

// The `from` is necessary to turn `observableQuery` into an RxJS observable
const result = await firstValueFrom(from(observableQuery));

I don't quite follow this example using from with Apollo Angular. Don't we already have an Observable wrapping ObservableQuery on valueChanges? I'm guessing as a substitute for use of result(), I can change add another subscriber to the valueChanges Observable?

@PowerKiKi I would have used the helper if it was there, but I'm not sure it's necessary. Once I knew that I could get the correct type by checking on dataState, I didn't really need it anymore.

If there was a global setting to disable partial results everywhere I would have used it, but if I need to go to each place and use the helper, I might just as well check on dataState directly.

Can you provide an example of how you narrowed the result using dataState to narrow types successfully, @JosephHalter? My simple-minded attempt to just test if (dataState === 'complete') {} in the Subscription next handler did not not eliminate a DeepPartialObject union.

Finally, it looks like the order of the generic parameters have been swapped for Query.FetchOptions, Query.WatchOptions, & Subscription.SubscribeOptions?

@jerelmiller
Copy link
Contributor

jerelmiller commented Sep 29, 2025

  • We had been using the generic ApolloCache to type the results in our mutation update methods. Is there a recommended way to type those results now?

That change comes from apollo-client. Their migration doc does not seem to cover your use-case. And I cannot recommend anything either. Maybe @jerelmiller could share some thoughts on that ?

The only thing that changed here is that ApolloCache no longer has a TSerialized generic argument. We found most users typed ApolloCache as ApolloCache<any> (or usually done through ApolloClient<any>) so that generic wasn't really useful (it only affected cache.restore() and cache.extract() anyways). Anywhere you reference ApolloCache, you should just need to remove the generic argument.

I don't quite follow this example using from with Apollo Angular

Sorry that text came straight from the changelog in core Apollo Client which is why that might have been confusing. The old result method in ObservableQuery was essentially a Promise that was fulfilled as soon as the first result was emitted from ObservableQuery.

You should be able to replicate this with firstValueFrom(queryRef.valueChanges) since valueChanges already applies from on observableQuery (see this change).

Note: notifyOnNetworkStatusChange now defaults to true and an initial loading state is emitted, so firstValueFrom might not be sufficient on its own. You might either need to turn off notifyOnNetworkStatusChange to avoid that loading state, or add a filter to ensure it only emits the result when the network status is settled.

My simple-minded attempt to just test if (dataState === 'complete') {} in the Subscription next handler did not not eliminate a DeepPartialObject union.

Could you provide a more full example of what you tried here? It only type narrows in the scope where that check occurs:

if (dataState === 'complete') {
  data
  // ^ TData
}

// this still includes `DeepPartial<TData>` since the `if` didn't `return`
data
// ^ TData | DeepPartial<TData> | undefined

You should be able to eliminate DeepPartial if you return early:

if (dataState !== 'complete') {
  return
}

data
// ^ TData

// --- or ---
// filter out partial results
if (dataState === 'partial') {
  return
}

data
// ^ TData | undefined

Finally, it looks like the order of the generic parameters have been swapped for Query.FetchOptions, Query.WatchOptions, & Subscription.SubscribeOptions?

Yes, this was a change to align with Apollo Client which updated all generic arguments to always be the order of <TData, TVariables>. There was a lot of inconsistency in the client where some used <TData, TVariables> and others <TVariables, TData>, so we wanted to make everything consistent. The same change was applied here.

@jerelmiller
Copy link
Contributor

jerelmiller commented Sep 29, 2025

Their migration doc does not seem to cover your use-case

@PowerKiKi Also good observation here! Let me make sure this gets added to the migration guide.

@jerelmiller
Copy link
Contributor

PR to add this to the migration guide here: apollographql/apollo-client#12949

@KeithGillette
Copy link
Contributor

Thanks, @jerelmiller!

Could you provide a more full example of what you tried here? It only type narrows in the scope where that check occurs:

Here's one of the simplest examples of type DeepPartialObject narrowing not working when testing dataState:

export const UserRead_Fields = gql`
	fragment UserFields on User {
		_id
		nameLast
		nameFirst
		username
		isVerified
		eventList {
			name
			dateTime
		}
	}
`;

export const UserRead_Query = gql`
	query UserRead {
		UserRead {
			...UserFields
		}
	}
	${UserRead_Fields}
`;

export interface IUserRead_ResponseData {
	UserRead: {
		_id: string;
		nameLast: string;
		nameFirst: string;
		username: string;
		isVerified: boolean;
		eventList: IUserEvent[];
	};
}

export interface IUser extends Partial<OmitMethodKeys<User>> {
}

export class User extends DomainEntityBase {
	public nameFirst: string;
	public nameLast: string;
	public username: string;
	public isVerified: boolean;
	public accountList: IAccount[] = [];
	public eventList: IUserEvent[] = [];
	public readonly __typename: DomainEntityTypeName.User = DomainEntityTypeName.User;

	public constructor(initialValues: IUserDocument) {
		super();
		if (initialValues) {
			populate<IUser>(this, { ...initialValues, ...initialValues.profile });
			if (!this.username) {
				this.username = initialValues.emails[0].address;
			}
			this.isVerified = initialValues.emails.find((emails) => {
				return this.username.toLocaleLowerCase() === emails.address.toLocaleLowerCase();
			}).verified;
		}
	}
}

export class UserProfileComponent implements OnInit {
	protected user: IUser;
	public constructor(private apollo: Apollo) {}
	public ngOnInit(): void {
		this.apollo.watchQuery<IUserRead_ResponseData>({ query: UserRead_Query }).valueChanges
			.subscribe(({ data: { UserRead: user }, dataState }) => {
				if (dataState === 'complete') { // just added this to try to eliminate `DeepPartialObject` 
					this.user = user;  // still get TS2322 below on this assignment
					this.nameFirst.patchValue(user.nameFirst);
					this.nameLast.patchValue(user.nameLast);
					this.username.patchValue(user.username);
			}
		});
	}
}
TS2322: Type
{
  _id: string;
  nameLast: string;
  nameFirst: string;
  username: string;
  isVerified: boolean;
  eventList: IUserEvent[];
} | DeepPartialObject<{
  _id: string;
  nameLast: string;
  nameFirst: string;
  username: string;
  isVerified: boolean;
  eventList: IUserEvent[];
}>
is not assignable to type IUser
Type
DeepPartialObject<{
  _id: string;
  nameLast: string;
  nameFirst: string;
  username: string;
  isVerified: boolean;
  eventList: IUserEvent[];
}>
is not assignable to type Partial<OmitMethodKeys<User>>
Types of property eventList are incompatible.
Type DeepPartialObject<IUserEvent>[] is not assignable to type IUserEvent[]
Type DeepPartialObject<IUserEvent> is not assignable to type IUserEvent
Property name is optional in type DeepPartialObject<IUserEvent> but required in type IUserEvent

Separately, the Query.fetch, Query.watch, Subscription.subscribe, & Mutation.mutate method signatures have all change, as well?

@PowerKiKi
Copy link
Collaborator

PowerKiKi commented Sep 30, 2025

I simplified your example and I think the destructuring prevents TypeScript from type narrowing:

interface IUserRead_ResponseData {
  UserRead: {
    _id: string;
    nameLast: string;
    nameFirst: string;
  };
}

// 💡💡💡 TYPE THE QUERY DIRECTLY SO YOU DON'T NEED TO REPEAT TYPING AT EACH CALL SITES
const UserRead_Query = gql<IUserRead_ResponseData, Record<PropertyKey, never>>`
  query UserRead {
    UserRead {
      _id
      nameLast
      nameFirst
    }
  }
`;

let theUser: IUserRead_ResponseData['UserRead'] | null = null;

function testFailing(apollo: Apollo) {
  apollo
    .watchQuery({ query: UserRead_Query })
    .valueChanges.subscribe(({ data: { UserRead: user }, dataState }) => { // 🛑🛑🛑 FAILING BECAUSE OF DESTRUCTURING
    if (dataState === 'complete') {
      theUser = user;
    }
  });
}

function testOK(apollo: Apollo) {
  apollo
    .watchQuery({ query: UserRead_Query })
    .valueChanges.subscribe(result => {
      if (result.dataState === 'complete') {
        theUser = result.data.UserRead; // 🟢🟢🟢 SUCCESS BECAUSE OF TYPE NARROWING
      }
  });
}

And yes signatures for Query, Mutation and Subscription have changed: https://github.com/the-guild-org/apollo-angular/blob/b9c62a5b4b3b10c408bfb8386286013051bce71d/.changeset/tough-masks-search.md

@KeithGillette
Copy link
Contributor

I simplified your example and I think the destructuring prevents TypeScript from type narrowing:

Weird, but you are absolutely correct, @PowerKiKi. If I don't destructure the result, the test against dataState properly narrows the type, eliminating DeepPartialObject. We destructure in virtually every Apollo Angular call, either in a map in the Observable pipe or else in its subscription handler, so this is going to take hundreds if not thousands of code changes.

// 💡💡💡 TYPE THE QUERY DIRECTLY SO YOU DON'T NEED TO REPEAT TYPING AT EACH CALL SITES

Thank you for the tip—we missed that gql was a generic! 🤦🏻‍♂️

And yes signatures for Query, Mutation and Subscription have changed: https://github.com/the-guild-org/apollo-angular/blob/b9c62a5b4b3b10c408bfb8386286013051bce71d/.changeset/tough-masks-search.md

Very helpful. Thank you, @PowerKiKi. We extended the Query & Subscription classes, overriding fetch, watch, & subscribe to add centralized standard error handling and other side-effects, so applying these signature updates is also going to take a similar level of code modification.

@jerelmiller
Copy link
Contributor

jerelmiller commented Sep 30, 2025

Quick note on the destructuring... it should work, but I think the problem here is that you're destructuring too deep so the type narrowing isn't working correctly in your example. The type narrowing happens on data in combination with dataState. In your example, you're going further and destructuring UserRead off of data and TypeScript can't narrow this correctly.

.subscribe(({ data: { UserRead: user }, dataState })

If you instead destructure just data and dataState, the type narrowing should work:

.subscribe(({ data, dataState }) => {
  if (dataState === 'complete') {
    theUser = data.UserRead
  }
})

I've tried this locally in the test suite and this works fine. Keeps some of the destructuring without having to repeat result. everywhere 🙂

@JosephHalter
Copy link

JosephHalter commented Sep 30, 2025

@jerelmiller You can destructure after the check, to continue on your example:

.subscribe(({ data, dataState }) => {
  if (dataState === 'complete') {
    const { UserRead: user } = data
    // more code here...
  }
})

@jerelmiller
Copy link
Contributor

@JosephHalter yep that works great too!

@PowerKiKi
Copy link
Collaborator

@KeithGillette for your destructuring thing... Wouldn't a helper function similar to what is suggested in #2367 (comment) make the migration easier for you ?

@KeithGillette
Copy link
Contributor

@KeithGillette for your destructuring thing... Wouldn't a helper function similar to what is suggested in #2367 (comment) make the migration easier for you ?

Yes, while it would require code changes in all the same places, I think simply inserting a helper function like the ones in those examples into the Observable pipe would require less work than (re)moving destructuring and inserting type-narrowing tests.

@PowerKiKi
Copy link
Collaborator

Would you expect that hypothetical helper always filter complete dataState, or would you also need to filter something else ? Eg: should we create a single "hardcoded" function like filterCompleteData() ? or a function with an arg that might look like only('complete') ? And any opinion on the helper name ?

@KeithGillette
Copy link
Contributor

Would you expect that hypothetical helper always filter complete dataState, or would you also need to filter something else ? Eg: should we create a single "hardcoded" function like filterCompleteData() ? or a function with an arg that might look like only('complete') ? And any opinion on the helper name ?

I like the flexibility and semantics of your hypothetical only helper function, though for our use case, I think we would only ever want complete data, so that flexibility would go unused. If you did release just a hard-coded helper function, I think onlyCompleteData() would be a clearer name.

@jerelmiller
Copy link
Contributor

jerelmiller commented Oct 1, 2025

@PowerKiKi I'm not sure there would be a need for anything other than complete, so filterCompleteData might be the best option. Most of the other dataState states (with some exception) are temporary. Not sure you'd really want to filter on a result that's only empty (data === undefined), partial (loaded from cache and is likely loading; cache-only is the exception here), or streaming (issued a @defer/@stream query that hasn't finished yet). So perhaps just a filterCompleteData would be enough?

Edit: might also be a good idea to recommend disabling notifyOnNetworkStatusChange if that helper would be used so that the source observable emits less notifications to begin with 🙂

@jerelmiller
Copy link
Contributor

jerelmiller commented Oct 1, 2025

Actually not true. streaming can also be useful here as well if you want to incrementally render your UI as parts of the data are streamed from the server (otherwise it voids the benefit of using it to begin with). Right now that type is implemented as just TData (it does nothing special), so it would work. Though I suppose you could just not use filterCompleteData() in that case 🤔

@tomachristian
Copy link

@PowerKiKi seems to be working for us 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants