🚀Introducing Apollo Orbit: A GraphQL Client for Angular with modular state management
What is Apollo Orbit for Angular?
Apollo Orbit is a fully-featured Apollo Client for Angular.
Apollo Client is a GraphQL client with advanced caching capabilities which can be used to fetch, cache, and modify application data, all while automatically updating your Angular UI.
Apollo Orbit brings the power of Apollo Client to Angular and combines it with Redux/NGXS concepts for state management.
For complete documentation and step-by-step guide, please visit Apollo Orbit — Angular Docs
Features ✨
- A fully-featured implementation of Apollo Client for Angular with a focus on optimal developer experience.
- 100% typesafe:
@apollo-orbit/codegen
package generates TypeScript code catching any breaking GraphQL schema changes at compile time across application, cache & state logic. - Comprehensive state management: Apollo Orbit combines the strengths of Apollo Client and traditional state management libraries, removing the need for an external state management library.
- Decoupling: Separate state management code from component code using modular
state
definitions andaction
handlers. - Modular: Each
state
definition manages its own slice of the cache. - Separation of concerns (SoC): Different
state
slices can handle the sameMutation
orAction
independently. - Event-driven architecture: Apollo Orbit actions enable event-driven application design.
Getting Started
Setup environment, then provide Apollo Orbit to your ApplicationConfig or AppModule
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, inject } from '@angular/core';
import { InMemoryCache, provideApolloOrbit, withApolloOptions } from '@apollo-orbit/angular';
import { HttpLinkFactory, withHttpLink } from '@apollo-orbit/angular/http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
provideApolloOrbit(
withHttpLink(),
withApolloOptions(() => ({
cache: new InMemoryCache(),
link: inject(HttpLinkFactory).create({ uri: 'http://localhost:4000/graphql' })
})
)
]
};
Queries
Let’s cover the basics by starting with a simple example of a component using Apollo Orbit to query data.
First, we write our GraphQL query:
fragment BookFragment on Book {
id
name
genre
authorId
}
query Books($name: String, $genre: String, $authorId: ID) {
books(name: $name, genre: $genre, authorId: $authorId) {
...BookFragment
}
}
Assuming you’ve followed the Getting Started guide, when saving this file, it will trigger GraphQL code generation using @apollo-orbit/codegen.
đź’ˇ @apollo-orbit/codegen is compliant with
TypedDocumentNode
standard which is supported by most GraphQL clients.
Visit the docs for more information about codegen.
Next, we write our component logic:
import { Apollo, mapQuery } from '@apollo-orbit/angular';
import { BooksQuery } from '../../graphql/types';
@Component({
selector: 'app-books',
template: `
<h3>Books <button (click)="refetch()">âźł</button></h3>
@if (books$ | async; as booksResult) {
@if (booksResult.loading) { Loading... }
@if (booksResult.error) { {{ booksResult.error.message }} }
@for (book of booksResult.data ?? booksResult.previousData; track book.id) {
<div>{{ book.name }}</div>
}
}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BooksComponent {
private readonly booksQuery = this.apollo.watchQuery(new BooksQuery());
protected readonly books$ = this.booksQuery.pipe(
mapQuery(data => data.books)
);
public constructor(
private readonly apollo: Apollo
) { }
protected refetch(): void {
this.booksQuery.refetch();
}
}
Let’s go through some of the features in the above code sample:
Seamless integration with RxJS
this.apollo.watchQuery(...)
returns a QueryObservable<TData, TVariables>
which extends RxJS
's standard Observable<QueryResult<TData>>,
this allows direct calls to Observable
members like subscribe
and pipe
and also ObservableQuery
members like refetch
, fetchMore
and subscribeToMore
...
Auto-resubscription on query observable error
If an error occurs while fetching data using watchQuery
, Apollo Orbit seamlessly manages the re-subscription to the underlying observable, ensuring that future operations, such as refetching or polling, proceed smoothly without any additional effort from the developer
previousData
Apollo Orbit exposes a previousData
property which stores the last non-nil value of the data
property that can be used to provide a smoother user experience when refetching data.
mapQuery
Apollo Orbit provides a helpermapQuery
RxJS operator to map both data
and previousData
while preserving other properties of QueryResult<T>
.
Mutations
Writing mutations using Apollo Orbit is as simple as working with queries.
Let’s start by writing our GraphQL mutation:
mutation AddBook($book: BookInput!) {
addBook(book: $book) {
...BookFragment
}
}
Next, we write our component logic:
import { Apollo, mapMutation } from '@apollo-orbit/angular';
import { AddBookMutation, BookInput } from '../../graphql/types';
@Component({
selector: 'new-book',
template: `
@if (error(); as error) {
<div>An error has occurred: {{ error.message }}</div>
}
<form [formGroup]="bookForm" (ngSubmit)="addBook(bookForm.value)">
...
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NewBookComponent {
protected readonly error = signal<Error | undefined>(undefined);
public constructor(
private readonly apollo: Apollo,
private readonly notificationService: NotificationService
) { }
protected addBook(book: BookInput): void {
this.error.set(undefined);
this.apollo.mutate(new AddBookMutation({ book })).pipe(
mapMutation(data => data.addBook)
).subscribe({
next: result => this.notificationService.success(`Book '${result.data.name}' was added successfully.`),
error: error => this.error.set(error)
});
}
...
}
Optional vs required query variables
The generated AddBookMutation
class constructor requires a mandatory variables parameter, unlikeBooksQuery
, which accepts an optional variables parameter.
This is automatically determined by @apollo-orbit/codegen
when a GraphQL field has mandatory variables, ensuring that any breaking changes in the GraphQL schema, even in field variables, are caught at compile-time.
Please refer to the docs if you’d like to learn more Queries, Mutations & Subscriptions.
State Management
Apollo Orbit leverages Apollo Client’s extensive caching capabilities to provide a comprehensive solution for managing both remote and local data state with ease.
Apollo Client utilizes GraphQL’s rich type information system (schema) in order to maintain an in-memory normalized cache of data retrieved from the API with no additional effort from the developer.
For example, let’s take the following updateBook
mutation:
mutation UpdateBook($id: ID!, $book: BookInput!) {
updateBook(id: $id, book: $book) {
...BookFragment
}
}
When apollo.mutate(new UpdateBookMutation({ id, book })
is called from a component, any active apollo.watchQuery(new BooksQuery())
observables on the page will receive a new value with the updated book data without any additional effort.
Alas, the same isn’t true for mutations that add a value to an existing collection, like addBook
mutation in the previous code sample. Those require a bit more work on the developer’s part, and it looks something like this:
export class BooksComponent {
public constructor(
private readonly apollo: Apollo,
) { }
protected addBook(book: BookInput): void {
this.apollo.mutate({
...new AddBookMutation({ book }),
// Handle cache update
update(cache, result) {
const addBook = result.data?.addBook;
if (!addBook) return;
// Update full list of books
cache.updateQuery(
new BooksQuery(),
data => ({ books: [...data.books, addBook] })
);
// Update author's list of books
cache.updateFragment(
identifyFragment(AuthorFragmentDoc, book.authorId),
author => ({ ...author, books: [...author.books, addBook] })
);
}
}).subscribe();
}
}
There are a few issues with the code above:
- Mixing of component and state logic: This can quickly clutter component code and increase complexity.
- Coupling of component and state logic: The component needs to be aware of parts of the state that might be managed by other modules e.g. author module.
- Inconsistent behaviour: Calling
addBook
mutation from a different component needs to ensure the same logic is executed, otherwise it may lead to inconsistent behaviour. - Test isolation: It’s difficult to test cache logic in isolation without testing the component logic.
State
Apollo Orbit elegantly solves the above issues by providing the ability to define modular state
slices.
Let’s start by defining two slices: one for managing the book slice of the cache and another for the author slice.
book.state.ts
import { state } from '@apollo-orbit/angular';
import { AddBookMutation, BooksQuery } from '../../graphql/types';
export const bookState = state(descriptor => descriptor
.mutationUpdate(AddBookMutation, (cache, info) => {
const addBook = info.data?.addBook;
if (!addBook) return;
cache.updateQuery(
new BooksQuery(),
data => ({ books: [...data.books, addBook] })
);
})
);
author.state.ts
import { identifyFragment, state } from '@apollo-orbit/angular';
import { AddBookMutation, AuthorFragmentDoc } from '../../graphql/types';
export const authorState = state(descriptor => descriptor
.mutationUpdate(AddBookMutation, (cache, info) => {
const addBook = info.data?.addBook;
if (!addBook) return;
const { authorId } = info.variables.book;
cache.updateFragment(
identifyFragment(AuthorFragmentDoc, authorId),
author => ({ ...author, books: [...author.books, addBook] })
);
})
);
identifyFragment
is a helper function provided by Apollo Orbit for returning a fragment object that uniquely identifies a fragment in the cache.
Next, we register the states with Angular’s DI system, by providing them in the providers
array of an NgModule
or a route
:
import { provideStates } from '@apollo-orbit/angular';
import { authorState } from './states/author.state';
import { bookState } from './states/book.state';
...
providers: [
provideStates(authorState, bookState)
]
...
Now, when a component calls:apollo.mutate(new AddBookMutation({ book })).subscribe()
the mutationUpdate
functions defined in our states are automatically executed and the UI displaying books and author books is updated.
The example above demonstrates how the same mutation can be handled independently by different states, achieving separation of concerns (SoC) and complete decoupling between component and state logic.
More state functionality
state
can do much more than just handle mutation updates, for example, we can extend book.state.ts to return an immediate optimisticResponse
while waiting for the server’s response and display a success/error notification as an effect
of the mutation:
import { state } from '@apollo-orbit/angular';
import { AddBookMutation, BooksQuery } from '../../graphql/types';
import shortid from 'shortid';
export const bookState: StateFactory = () => {
const notificationService = inject(NotificationService);
return state(descriptor => descriptor
...
.optimisticResponse(AddBookMutation, ({ book }) => ({
__typename: 'Mutation' as const,
addBook: {
__typename: 'Book' as const,
id: shortid.generate(),
genre: book.genre,
name: book.name,
authorId: book.authorId
}
}))
.effect(AddBookMutation, info => {
if (info.data?.addBook) {
notificationService.success(`New book '${info.data.addBook.name}' was added successfully.`);
} else if (info.error) {
notificationService.error(`Failed to add book '${info.variables?.book.name}': ${info.error.message}`);
}
})
);
};
For more information, refer to Apollo Orbit’s state docs
Actions
Besides managing the state of data fetched from a remote API, it’s common to implement state that is managed entirely on the client.
This is where actions are useful in communicating commands or events that occur within a component to the various state
slices.
As a demonstration of how actions work, let’s implement a theme toggling feature.
In the interest of brevity, I’ll demonstrate the code as a whole here, but feel free to visit the docs for a step-by-step guide.
Let’s start by defining our actions:
theme.actions.ts
import { ThemeName } from '../../graphql';
export class ToggleThemeAction {
public static readonly type = '[Theme] ToggleTheme';
public constructor(
public readonly force?: ThemeName
) { }
}
export class ThemeToggledAction {
public static readonly type = '[Theme] ThemeToggled';
public constructor(
public readonly toggles: number
) { }
}
Next, we define the state:
theme.state.ts
import { inject } from '@angular/core';
import { gql, StateFactory, state } from '@apollo-orbit/angular';
import { ThemeName, ThemeQuery } from '../../graphql/types';
import { NotificationService } from '../../services/notification.service';
export class ToggleThemeAction {
public static readonly type = '[Theme] ToggleTheme';
public constructor(
public readonly force?: ThemeName
) { }
}
export class ThemeToggledAction {
public static readonly type = '[Theme] ThemeToggled';
public constructor(
public readonly toggles: number
) { }
}
export const themeState: StateFactory = () => {
const notificationService = inject(NotificationService);
return state(descriptor => descriptor
// Define local state schema
.typeDefs(gql`
type Theme {
name: ThemeName!
displayName: String!
toggles: Int!
}
enum ThemeName {
DARK_THEME
LIGHT_THEME
}
extend type Query {
theme: Theme!
}
`)
// Implement displayName field on Theme type
.typePolicies({
Theme: {
fields: {
displayName: (existing, { readField }) =>
readField<ThemeName>('name') === ThemeName.LightTheme
? 'Light'
: 'Dark'
}
}
})
// Set initial state
.onInit(cache => {
cache.writeQuery({
...new ThemeQuery(),
data: {
theme: {
__typename: 'Theme',
name: ThemeName.LightTheme,
toggles: 0,
displayName: 'Light'
}
}
});
})
// Handle ToggleThemeAction
.action(ToggleThemeAction, (action, { cache, dispatch }) => {
const result = cache.updateQuery(
new ThemeQuery(),
data => ({
theme: {
...data.theme,
toggles: data.theme.toggles + 1,
name: action.force ?? (data.theme.name === ThemeName.DarkTheme ? ThemeName.LightTheme : ThemeName.DarkTheme)
}
})
);
return dispatch(new ThemeToggledAction(result?.theme.toggles as number));
})
// Handle ThemeToggledAction
.action(ThemeToggledAction, (action, context) => {
notificationService.success(`Theme was toggled ${action.toggles} time(s)`);
})
);
};
Now, that we’ve defined the local GraphQL schema usingtypeDefs
we can write our GraphQL query and save it to trigger codegen.
query Theme {
theme @client {
name
toggles
displayName
}
}
Next, we register the state as shown previously and create a theme toggling component:
import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { Apollo } from '@apollo-orbit/angular';
import { map } from 'rxjs';
import { ThemeQuery } from '../graphql/types';
import { ToggleThemeAction } from '../states/theme/theme.actions';
@Component({
selector: 'app-theme',
standalone: true,
imports: [AsyncPipe],
template: `
@if (theme$ | async; as theme) {
<div>
<span>Current theme:</span>
<b>{{ theme.displayName }}</b>
<button type="button" (click)="toggleTheme()">Toggle theme</button>
</div>
}
`
})
export class ThemeComponent {
protected readonly theme$ = this.apollo.cache.watchQuery(new ThemeQuery()).pipe(
map(({ data }) => data.theme)
);
public constructor(
private readonly apollo: Apollo
) { }
protected toggleTheme(): void {
this.apollo.dispatch(new ToggleThemeAction());
}
}
Summary
In this introduction, we’ve covered some of the basic features of Apollo Orbit — Angular.
Make sure to visit the docs for many more features like:
- Apollo Links: HTTP, HTTP Batch Link, Persisted Queries, etc…
- Server-side rendering
- Multi-client support
- Ability to provide custom
ApolloClient
implementation - Or if you’re only interested in using the core functionality of Apollo Orbit without any of the state management capabilities then you can do that too.
I hope Apollo Orbit proves as valuable for your Angular projects as it has for ours and I look forward to hearing your feedback.