Write a Basic GraphQL Client for NgRx

First Published: May 08, 2020

One question both Mike Ryan and I get asked often is whether NgRx is compatible with GraphQL. I think the confusion comes from the fact that many popular GraphQL clients can also handle local state management. The assumption is then that, since NgRx handles state management for you, you wouldn't use GraphQL alongside NgRx.

In this stream, Mike and I walk through setting up a very basic GraphQL client for NgRx. Hopefully this helps you "define the edges" around what GraphQL handles, what NgRx handles, and why robust GraphQL clients are needed. Please understand, though, that this is meant to be an educational exercise. A production app would require a lot of development for this client to ensure that it is secure and performant. That's the stuff libraries like Apollo work on every day!

Let's walk through the GraphQL stack to understand what's going on.

GraphQL is a query language. It's concerned with data access. GraphQL is totally agnostic of whatever language or technology you are using. Even though it was created by Facebook, it is run independently and has a spec that gets updated regularly.

When you write a GraphQL schema on your server, something needs to translate between that schema and your database. You can either write that yourself or offload that to tools like Prisma and Hasura (Hasura does a bit more than that, but that's a subject for another day).

You can then run a server like Apollo Server to make your entire GraphQL schema available as a single endpoint to your frontend. Sometimes you'll see this as /graphql, other times it will be the root of the server (/). The frontend calls the GraphQL endpoint with a POST request. In the body of that request is the query string, which looks like this:

query getHabits {
habits {
id
description
points
entries {
id
date
notes
completed
}
}
}

On the frontend, libraries like Apollo Client and Relay make that request for you, but also do a bunch of other things that make your life easier. These include caching your data, polling the server, and providing extra developer tooling. One thing they can also do is help with your local state management such as handling errors and updating the UI during and after loading.

What does this mean for NgRx? Since GraphQL is just a data access technology, it means that NgRx doesn't really care whether you're using REST or GraphQL. You can still use Angular's HttpClient to make a POST request to your GraphQL endpoint and let NgRx handle the data through Effects, Reducers, and Actions.

To implement the basic GraphQL service, you'll just use a POST call:

// graphql.service.ts
@Injectable({ providedIn: 'root' })
export class GraphQLService {
constructor(readonly http: HttpClient) {}
fetch(query: string, variables: object = {}) {
return this.http.post('/graphql', { query, variables })
}
}

Note that you would use the same fetch method to send both queries and mutations to your server. It's a bit confusing that the parameter the server is expecting is called query regardless of what you're sending. One thing you could do to make this developer experience a bit nicer is to wrap that fetch function into query and mutation functions. This would also allow any sort of customization you'd want to do for either scenario.

In the stream, we also did a quick hack to trick Visual Studio Code into giving us syntax highlighting by creating a gql function:

// graphql.service.ts
export function gql(stringPieces: TemplateStringsArray): string {
return stringPieces.join('')
}

We can then use that function in our Effect, where we'll call the GraphQLService:

// app.effects.ts
const getHabitsQuery = gql`
query getHabits {
habits {
id
description
points
entries {
id
date
notes
completed
}
}
}
`
@Injectable()
export class AppEffects {
constructor(readonly graphql: GraphQLService, readonly actions$: Actions) {}
getHabits$ = createEffect(() => {
// Listen for the "enter home page action"
// then issue the GraphQL request
return this.actions$.pipe(
ofType(AppActions.enterHomePageAction),
exhaustMap(() => {
return this.graphql
.fetch(getHabitsQuery)
.pipe(
map((response: any) =>
AppActions.getHabitsSuccessAction(response.data.habits),
),
)
}),
)
})
}

We're using exhaustMap here in order to ignore any additional requests until the first one completes. To learn more about exhaustMap, concatMap, and other operators, check out our joint talk from RxJS Live 2019.

Once we've got that data set as the payload from the getHabitsSuccessAction, we can add a case to the reducer:

// habits.reducer.ts
export const habitsReducer = createReducer(
habitsInitialState,
on(
AppActions.getHabitsSuccessAction,
(state, action): HabitsShape => {
return {
...state,
habits: action.habits,
}
},
),
)

Finally, to read the data from the store, we can set up a selector in the same file:

// habits.reducer.ts
export const selectHabits = (state: HabitsShape) => state.habits

And then add it to our global selectors:

// app.state.ts
export const selectHabitsState = (state: AppState) => state.habits
export const selectHabits = createSelector(
selectHabitsState,
HabitsState.selectHabits,
)

We're then able to inject the store in the component and read the data:

// app.component.ts
constructor(store: Store<AppState.AppShape>) {
this.habits$ = store.select(AppState.selectHabits);
store.dispatch(AppActions.enterHomePageAction());
}

There's a long way to go before this GraphQL client could be used in the real world. One last thing we experimented with before the end of the stream was adding a simple polling Effect:

// app.effects.ts
pollForHabits$ = createEffect(() => {
return interval(2_500).pipe(
exhaustMap(() => {
return this.graphql
.fetch(getHabitsQuery)
.pipe(
map((response: any) =>
AppActions.getHabitsSuccessAction(response.data.habits),
),
)
}),
)
})

This will ensure that our list of habits is up to date by checking with the sever every 2.5 seconds and refreshing the data with the getHabitsSuccessAction.

(Side note: did you know you can add the _ character to help make milliseconds more readable? 🤯)

Check out the full repo on GitHub to see the rest of the code. To get notified the next time we work on this project, follow me on Twitch.

Happy coding! 👋

Project list gathering dust? 👀

Drop me a line below so I can send you the Tiny Experiments framework and worksheet. It's a simple process to help you finish what you start. You'll also join over 2100 other devs and dev advocates on the Developer Microskills newsletter. In each issue of Developer Microskills, I share a practical, actionable way to grow in your career or be more effective in your work. No motivational speeches, no hand-waving, and no spam, but also no infuriating sense of self-importance that plagues so much career and productivity advice out there.

Sam Julien © 2015 - Present