Dojo GraphQL
Rene Rubalcava | June 16, 2019
GraphQL has grown in popularity over the past couple of years. Where GraphQL shines is in its descriptive nature of querying data.
If you want to write a query for the Star Wars API to get all the film titles, it might look something like this.
{
allFilms{
edges{
node{
title
}
}
}
}
The query is JSON-like, but it's not JSON. You can learn more about GraphQL on the tutorials page.
Apollo provides a client API you can use to work with GraphQL. It saves you some work of writing your own POST requests, so I highly recommend you learn it. Apollo provides libraries to integrate with Angular and React, but so far not one for Dojo. But that's ok, because you can use the Apollo Client to build your own GraphQL integration.
Looking at react-apollo, they have an ApolloProvider
that you can use to wrap components of your application. This provides the Apollo client to components. Those components can then use a Query
higher order component to pass the query and client and thus display the result.
How hard can that be?
Trust the Process
When working with Dojo, most of your work with external APIs is probably going to be done in a process. We covered this topic in detail in this post.
Here is what a generic process for working with GraphQL might look like.
// src/processes/apolloProcess.ts
import {
createProcess,
createCommandFactory
} from "@dojo/framework/stores/process";
import { add } from "@dojo/framework/stores/state/operations";
const commandFactory = createCommandFactory<{
client: any; // this will be the apollo client
data: any; // this will be graphql result
loaded: boolean; // keep track if the data has been loaded yet
}>();
const fetchData = commandFactory(async ({ path, payload }) => {
const { client, query } = payload;
const { data } = await client.query({ query });
return [add(path("data"), data), add(path("loaded"), true)];
});
export const fetchDataProcess = createProcess("fetch-data", [fetchData]);
This process will take a given apollo client instance and a GraphQl query to fetch some results. This works pretty well because it's not tied to any particular endpoint or data structure, even though it is currently typed as any
for client and data. I could try to work around that with some generics, but wanted to keep this example fairly simple.
Put it in a box
We can tie this together with a widget and Dojo container.
// src/containers/QueryContainer.tsx
import { tsx } from "@dojo/framework/widget-core/tsx";
import { WidgetBase } from "@dojo/framework/widget-core/WidgetBase";
import { Store } from "@dojo/framework/stores/Store";
import { StoreContainer } from "@dojo/framework/stores/StoreInjector";
import { fetchDataProcess } from "../processes/apolloProcess";
// Use the ApolloClient for typing
import ApolloClient from "apollo-boost";
interface QueryProps {
client?: ApolloClient<any>;
query: string;
data?: any;
loaded?: boolean;
fetchData?: (args: any) => void;
}
// base widget that handles displaying children that use the Query
export class BaseQuery extends WidgetBase<QueryProps, any> {
onAttach() {
const { client, query, loaded, fetchData } = this.properties;
// if the data has not been loaded yet
// and we have a query, lets get some data
if (!loaded && query) {
fetchData({ client, query });
}
}
protected render() {
const { loaded, data } = this.properties;
return this.children.map(child => {
// if the child nodes are a function,
// call the function with data from the
// GraphQL process
if (typeof child === "function") {
return child({ loading: !loaded, data });
}
// or just return a regular node
return child;
});
}
}
function getProperties(store: Store<{ data: any; loaded: boolean }>): any {
const { get, path } = store;
// pass the Dojo store properties and methods to the widget
return {
data: get(path("data")),
loaded: get(path("loaded")),
fetchData: fetchDataProcess(store)
};
}
// Use a StoreContainer
export const Query = StoreContainer(BaseQuery, "state", {
getProperties
});
In this snippet we provide a BaseQuery
that is going to handle taking any queries that child widgets might provide and use those queries to to fetch some data. This widget uses a StoreContainer
to pass the store that is updated using our process to the BaseQuery
. We can call this container a Query
to keep it simple. This is going to allow us to write some code like the following.
export class MyWidget extends WidgetBase<{ client: any }> {
protected render() {
const { client } = this.properties;
return (
// use our Query Widget with the client it's given and
// a query we have written
<Query query={query} client={client}>
{({ loading, data }) => {
if (loading) {
return <span>Loading...</span>;
} else {
return <div classes={[css.root]}>{parseMyData(data)}</div>;
}
}}
</Query>
);
}
}
Be a good provider
At this point you might be asking yourself, How do I pass a client to a widget that uses this?
Good question. Technically, you could create the client in your Widget module and provide it to <Query>
. But that seems kind of icky to bind backend concerns into my UI code. The way react-apollo
does this is by providing an <ApolloProvider>
that you can give a client
and then wrap your application components with it. These components will have access to the Apollo client
to give to the Query
higher order component.
It basically looks like the <ApolloProvider>
provides its client
property to child widgets. I can do that.
// src/providers/ApolloProvider.tsx
import { tsx } from "@dojo/framework/widget-core/tsx";
import { WidgetBase } from "@dojo/framework/widget-core/WidgetBase";
export class ApolloProvider extends WidgetBase<{ client: any }> {
protected render() {
const { client } = this.properties;
for (let child of this.children) {
if ((child as any).properties) {
// inject the client of the provider into each child
// widget
(child as any).properties.client = client;
}
}
return this.children;
}
}
What this naive ApolloProvider
does is iterates over the children
of the widget and injects the client
property into each one. I'm sure the react-apollo
implementation does much more, but I'm not going to argue with what works.
Now that I have my provider, I can start to tie it all together.
The great provider
In my main.tsx
where I initialize my Dojo application, I can create my ApolloClient
and pass it my ApolloProvider
that will wrap my other widgets so that I can use it.
// src/main.tsx
...
import { Store } from "@dojo/framework/stores/Store";
import { registerStoreInjector } from "@dojo/framework/stores/StoreInjector";
import ApolloClient from "apollo-boost";
import { ApolloProvider } from "./providers/ApolloProvider";
import { Countries } from "./widgets/Countries";
// initialize a GraphQL client
const client = new ApolloClient({
uri: "https://countries.trevorblades.com"
});
const store = new Store();
const registry = registerStoreInjector(store);
class App extends WidgetBase {
protected render() {
// pass the client to the ApolloProvider
// The <Countries /> widget will use it
return (
<div>
<ApolloProvider client={client}>
<h2>{"\u2728 dojo-apollo \u2728"}</h2>
<Countries />
</ApolloProvider>
</div>
);
}
}
...
The sample GraphQL API I am going to use provides a list of Countries. So I'm going to write a widget that can display those results.
GraphQL results
Here is where we get to see the fruit of our labors! We can write a widget that will display a specific set of data from our GraphQL API. So the widget can provide its own GraphQL query. This makes sense when you think of the widget as owning this query.
import { tsx } from "@dojo/framework/widget-core/tsx";
import { WidgetBase } from "@dojo/framework/widget-core/WidgetBase";
import gql from "graphql-tag";
import * as css from "./styles/Countries.m.css";
import { Query } from "../containers/QueryContainer";
interface Country {
name: string;
code: number;
}
// initialize a GraphQL query
export const query = gql`
{
countries {
name
code
}
}
`;
// helper method to display each country as a list item
// each country will link to a wikipedia page
const countryItems = (countries: Country[] = []) =>
countries.map(({ name, code }) => (
<li classes={[css.item]} key={code}>
<a
classes={[css.link]}
key={code}
href={`https://en.wikipedia.org/wiki/${name}`}
target="_blank"
>
{name}
</a>
</li>
));
export class Countries extends WidgetBase<{ client?: any }> {
protected render() {
// this is the `client` that was injected by the `<ApolloProvider>`
const { client } = this.properties;
return (
<Query query={query} client={client}>
{({ loading, data }) => {
// if data is still loading, show a message
if (loading) {
return <span>Loading...</span>;
} else {
// when data is done loading, display the list
return <ul classes={[css.root]}>{countryItems(data.countries)}</ul>;
}
}}
</Query>
);
}
}
This widget uses our Query
container to wrap up the part of the widget that relies on the GraphQL results. This looks pretty much exactly like react-apollo
.
You can see this entire example in action in this code sandbox.
Summary
This is a fairly simple implementation of a GraphQL <Query>
and <ApolloProvider>
, but it works pretty well in a case like this. If you have multiple different queries you want to run in a single application, I think you would need to create a factory method for your containers to define multiple states that would contain different results.
This is definitely something I want to continue working on in the future and I think there might be some more Dojo way of handling this in some features that look to be coming to Dojo in the future.
As always, have fun with it and keep on hacking!