Local State
Prerequisites
This article assumes you're familiar with Apollo Client's local state management capabilities.
Overview
In this article, we'll go through a complete example of managing local state with Apollo Orbit, including setting up the cache, defining local state, and updating the cache.
In this example, we'll build a theme management feature that allows users to switch between light and dark themes.
1. Define theme state
slice
Define local schema
First, we'll start by defining the local schema for our theme management feature, by creating a theme state
slice and defining the schema in typeDefs
.
import { gql, state } from '@apollo-orbit/angular';
export const themeState = () => {
return state(descriptor => descriptor
.typeDefs(gql`
type Theme {
name: ThemeName!
displayName: String!
toggles: Int!
}
enum ThemeName {
DARK_THEME
LIGHT_THEME
}
extend type Query {
theme: Theme!
}
`)
);
};
Saving this file will trigger the codegen of ThemeState
class in graphql/types.ts
file as per our setup.
Define GraphQL query
Next, we'll define a GraphQL query to fetch the current theme from the cache.
query Theme {
theme @client {
name
toggles
displayName
}
}
Initialise cache
Next, we'll define onInit
function to initialise the cache with the default theme.
import { state } from '@apollo-orbit/angular';
import { ThemeName, ThemeQuery } from '../../graphql/types';
export const themeState = () => {
return state(descriptor => descriptor
.onInit(cache => {
cache.writeQuery({
...new ThemeQuery(),
data: {
theme: {
__typename: 'Theme',
name: ThemeName.LightTheme,
toggles: 0,
displayName: 'Light'
}
}
});
})
);
};
Define actions
Next, we'll create theme.actions.ts
file to define actions for toggling the theme.
import { ThemeName } from '../../graphql/types';
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
) { }
}
Define action handles
import { inject } from '@angular/core';
import { state } from '@apollo-orbit/angular';
import { ThemeName, ThemeQuery } from '../../graphql/types';
import { NotificationService } from '../../services/notification.service';
import { ThemeToggledAction, ToggleThemeAction } from './theme.actions';
export const themeState = () => {
const notificationService = inject(NotificationService);
return state(descriptor => descriptor
.action(ToggleThemeAction, (action, { cache, dispatch }) => {
const result = cache.updateQuery(new ThemeQuery(), data => data
? {
theme: {
...data.theme,
toggles: data.theme.toggles + 1,
name: action.force ?? (data.theme.name === ThemeName.DarkTheme ? ThemeName.LightTheme : ThemeName.DarkTheme)
}
}
: data);
return dispatch(new ThemeToggledAction(result?.theme.toggles as number));
})
.action(ThemeToggledAction, (action, context) => {
notificationService.success(`Theme was toggled ${action.toggles} time(s)`);
})
);
};
Define local field policies
Finally, we'll define local field policy for the displayName
field.
import { state } from '@apollo-orbit/angular';
import { ThemeName } from '../../graphql/types';
export const themeState = () => {
return state(descriptor => descriptor
.typePolicies({
Theme: {
fields: {
displayName: (existing, { readField }) => readField<ThemeName>('name') === ThemeName.LightTheme ? 'Light' : 'Dark'
}
}
})
);
};
2. Provide theme state
Next, we'll provide the themeState
to provideGraphQL
provider (or GraphQLModule).
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
import { provideApolloOrbit, withStates } from '@apollo-orbit/angular';
import { themeState } from '../states/theme/theme.state';
export function provideGraphQL(): EnvironmentProviders {
return makeEnvironmentProviders([
provideApolloOrbit(
...
withStates(themeState)
)
]);
}
3. Create theme component
Finally, we'll bring it all together by defining our theme component which queries the cache and dispatches the relevant actions.
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
There you have it! we've created our first fully local state managed feature with Apollo Orbit.
Here's the complete theme.state.ts
code for reference
import { inject } from '@angular/core';
import { gql, state } from '@apollo-orbit/angular';
import { ThemeName, ThemeQuery } from '../../graphql/types';
import { NotificationService } from '../../services/notification.service';
import { ThemeToggledAction, ToggleThemeAction } from './theme.actions';
export const themeState = () => {
const notificationService = inject(NotificationService);
return state(descriptor => descriptor
.typeDefs(gql`
type Theme {
name: ThemeName!
displayName: String!
toggles: Int!
}
enum ThemeName {
DARK_THEME
LIGHT_THEME
}
extend type Query {
theme: Theme!
}
`)
.typePolicies({
Theme: {
fields: {
displayName: (existing, { readField }) => readField<ThemeName>('name') === ThemeName.LightTheme ? 'Light' : 'Dark'
}
}
})
.onInit(cache => {
cache.writeQuery({
...new ThemeQuery(),
data: {
theme: {
__typename: 'Theme',
name: ThemeName.LightTheme,
toggles: 0,
displayName: 'Light'
}
}
});
})
.action(ToggleThemeAction, (action, { cache, dispatch }) => {
const result = cache.updateQuery(new ThemeQuery(), data => data
? {
theme: {
...data.theme,
toggles: data.theme.toggles + 1,
name: action.force ?? (data.theme.name === ThemeName.DarkTheme ? ThemeName.LightTheme : ThemeName.DarkTheme)
}
}
: data);
return dispatch(new ThemeToggledAction(result?.theme.toggles as number));
})
.action(ThemeToggledAction, (action, context) => {
notificationService.success(`Theme was toggled ${action.toggles} time(s)`);
})
);
};