Skip to main content

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.

states/theme/theme.state.ts
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.

states/theme/gql/theme.graphql
query Theme {
theme @client {
name
toggles
displayName
}
}

Initialise cache

Next, we'll define onInit function to initialise the cache with the default theme.

states/theme/theme.state.ts
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.

states/theme/theme.actions.ts
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

states/theme/theme.state.ts
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.

states/theme/theme.state.ts
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).

graphql/graphql.provider.ts
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.

theme/theme.component.ts
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>
&nbsp;
<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
states/theme/theme.state.ts
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)`);
})
);
};