GraphQL Queries
With Apollo Client set up and connected to your Dgraph Cloud backend, and React routing set up, you can move on to the GraphQL queries that get the data to render the main pages.
You’ll use GraphQL Code Generator to generate typed React hooks that help contact the GraphQL endpoint, and then use those hooks in the React components to get the data.
GraphQL Code Generator
The Apollo Client libraries give you generic React hooks to contact GraphQL backends, but GraphQL Code Generator takes that to the next level by using GraphQL introspection to generate types with hooks specific to the API you are using. That means all your GraphQL calls are typed and if anything ever changes, you’ll know at development time.
Firstly, add all the GraphQL Code Generator dependencies as development dependencies to the project with:
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo @graphql-codegen/add @graphql-codegen/near-operation-file-preset @graphql-codegen/named-operations-object
You can then run the following command to to set up GraphQL Code Generator for the project:
yarn graphql-codegen init
> ... answer questions ...
However, you can skip the setup steps and jump straight to using it by adding a
file, codegen.yml
, in the top-level project directory. The following is the
configuration needed for this project. Remember to replace
<<Dgraph Cloud-GraphQL-URL>>
with the URL or your Dgraph Cloud endpoint.
overwrite: true
schema: "<<Dgraph Cloud-GraphQL-URL>>"
documents:
- 'src/**/*.graphql'
generates:
src/types/graphql.ts:
plugins:
- typescript
src/:
preset: near-operation-file
presetConfig:
baseTypesPath: types/graphql
folder: types
extension: .ts
plugins:
- typescript-operations
- typescript-react-apollo
- named-operations-object
config:
reactApolloVersion: 3
withHOC: false
withHooks: true
withComponent: false
That configuration tells GraphQL Code Generator to introspect the schema of your GraphQL API, generate using the typescript
plugin and place the generated code near-operation-file
(we’ll see what that means just below).
Then, add "generate-types": "graphql-codegen --config codegen.yml"
to the scripts key in package.json, so it now looks like:
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"generate-types": "graphql-codegen --config codegen.yml"
}
Now, whenever the schema of your GraphQL database changes, you can regenerate the project’s types with:
yarn run generate-types
Running that now, won’t do anything though, because you have to start your GraphQL development first.
GraphQL operations
You can layout the source of a Dgraph Cloud project however you wish. For this tutorial you’ll use the following project structure.
public
scripts
src
components
component1.tsx
component2.tsx
operations.graphql
types
operations.ts
...
types
graphql.ts
You’ll write GraphQL queries and mutations in the operations.graphql
file. Then, run GraphQL Code Generator and it generates the src/types/graphql.ts
file with global types for the things that make sense globally and src/components/types/operations.ts
for things that are local to the components.
Having operations.graphql
file in the directory for the components that it applies to makes it really easy to find the GraphQL (rather than it being split as strings in a number of javascript files) while still making it clear what components the GraphQL applies to. If your project gets larger, you might end up with more project structure and more operations files, but the general process still works.
Start by creating the scr/components/operations.graphql
file and add a query to find the data for home page’s list of posts.
query allPosts {
queryPost(order: { desc: datePublished }) {
id
title
tags
datePublished
category {
id
name
}
author {
username
displayName
avatarImg
}
commentsAggregate {
count
}
}
}
Then run:
yarn run generate-types
…and GraphQL Code Generator will create the src/types/graphql.ts
and src/components/types/operations.ts
files. If your interested in what was generated, open up those files and you’ll see how much the code generator did. If you want to use that to build a UI, read on.
GraphQL React hooks
Of the things that GraphQL Code Generator built after introspecting your GraphQL endpoint, it’s the React hooks you’ll use most in building a UI.
From the allPosts
query in the operations.graphql
file, GraphQL Code Generator built a hook useAllPostsQuery
with everything you need to make that GraphQL query.
In general, you’ll use it like this
const { data, loading, error } = useAllPostsQuery()
if (loading) { /* render loading indicator */ }
if (error) { /* handle error */ }
// layout using 'data'
The data
result will have exactly the same structure as the allPosts
operation, and it’s typed, so you can layout with confidence by for example using map
on the post list returned by queryPost
and then indexing into each post.
data?.queryPost?.map((post) => {
...
post?.author.displayName
...
}
Because of the types, you really can’t go wrong.
Layout with GraphQL - post list
Now that you have GraphQL to help write queries and get data and GraphQL Code Generator to turn that into typed Javascript, you can now layout your data and be sure you won’t make a mistake because GraphQL and types will catch you.
Let’s make a PostFeed
component that uses the useAllPostsQuery
and renders the data into a Semantic React UI Table
.
import React from "react"
import {
Header,
Label,
Loader,
Image,
Table,
Container,
} from "semantic-ui-react"
import { useAllPostsQuery } from "./types/operations"
import { Link } from "react-router-dom"
import { avatar } from "./avatar"
export function PostFeed() {
const { data, loading, error } = useAllPostsQuery()
if (loading) return <Loader active />
if (error) {
return (
<Container text className="mt-24">
<Header as="h1">Ouch! That page didn't load</Header>
<p>Here's why : {error.message}</p>
</Container>
)
}
const items = data?.queryPost?.map((post) => {
const likes = Math.floor(Math.random() * 10)
const replies = post?.commentsAggregate?.count
const tagsArray = post?.tags?.trim().split(/\s+/) || []
return (
<Table.Row key={post?.id}>
<Table.Cell>
<Link
to={{
pathname: "/post/" + post?.id,
}}
>
<Header as="h4" image>
<Image src={avatar(post?.author.avatarImg)} rounded size="mini" />
<Header.Content>
{post?.title}
<Header.Subheader>{post?.author.displayName}</Header.Subheader>
</Header.Content>
</Header>
</Link>
</Table.Cell>
<Table.Cell>
<span className="ui red empty mini circular label"></span>{" "}
{" " + post?.category.name}
</Table.Cell>
<Table.Cell>
{tagsArray.map((tag) => {
if (tag !== "") {
return (
<Label as="a" basic color="grey" key={tag}>
{tag}
</Label>
)
}
return " "
})}
</Table.Cell>
<Table.Cell>
<p>
<i className="heart outline icon"></i> {likes} Like
{likes === 1 ? "" : "s"}
</p>
<p>
{" "}
<i className="comment outline icon"></i> {replies}{" "}
{replies === 1 ? "Reply" : "Replies"}
</p>
</Table.Cell>
</Table.Row>
)
})
return (
<>
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.HeaderCell>Posts</Table.HeaderCell>
<Table.HeaderCell>Category</Table.HeaderCell>
<Table.HeaderCell>Tags</Table.HeaderCell>
<Table.HeaderCell>Responses</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>{items}</Table.Body>
</Table>
</>
)
}
There’s some layout and CSS styling in there, but the actual data layout is just indexing into the queried data with post?.title
, post?.author.displayName
, etc.
Note that the title of the post is made into a link with the following:
<Link to={{pathname: "/post/" + post?.id}}> ... </Link>
When clicked, this link will go through the React router to render the post component.
You can add whatever avatar links you like into the data, and you’ll do that
later in the tutorial after you add authorization and logins; but for now, make
a file src/components/avatar.ts
and fill it with this function that uses
random avatars we’ve supplied with the app boilerplate, as follows:
export function avatar(img: string | null | undefined) {
return img ?? "/" + Math.floor(Math.random() * (9 - 1) + 1) + ".svg"
}
Then, update the src/components/home.tsx
component to render the post list, as
follows:
import React from "react"
import { PostFeed } from "./posts"
export function Home() {
return <div className="layout-margin">{PostFeed()}</div>
}
With this much in place, you will see a home screen (start the app with yarn start
if you haven’t already) with a post list of the sample data you have added to your Dgraph Cloud database.
Each post title in the post list is a link to /post/0x...
for the id of the post. At the moment, those like won’t work because there’s not component to render the post. Let’s add that component now.
Layout of a post with GraphQL
Adding a new component that relies on different data is a matter of adding the right query to src/components/operations.graphql
, regenerating with GraphQL Code Generator, and then using the generated hook to layout a component.
Add a GraphQL query that gets a particular post by it’s id to src/components/operations.graphql
with this GraphQL query.
query getPost($id: ID!) {
getPost(id: $id) {
id
title
text
tags
datePublished
category {
id
name
}
author {
username
displayName
avatarImg
}
comments {
id
text
commentsOn {
comments {
id
text
author {
username
displayName
avatarImg
}
}
}
author {
username
displayName
avatarImg
}
}
}
}
Then, regenerate with:
yarn run generate-types
…and you’ll be able to use the useGetPostQuery
hook in a component. The difference with the previous hook is that useGetPostQuery
relies on a variable id
to query for a particular post. You’ll use React router’s useParams
to get the id passed to the route and then pass that to useGetPostQuery
like this:
const { id } = useParams<PostParams>()
const { data, loading, error } = useGetPostQuery({
variables: { id: id },
})
Laying out the post component is then a matter of using the data
from the hook to layout an interesting UI. Edit the src/components/post.tsx
component, so it lays out the post’s data like this:
import React from "react"
import { useParams } from "react-router-dom"
import {
Container,
Header,
Loader,
Image,
Label,
Comment,
} from "semantic-ui-react"
import { useGetPostQuery } from "./types/operations"
import { DateTime } from "luxon"
import { avatar } from "./avatar"
interface PostParams {
id: string
}
export function Post() {
const { id } = useParams<PostParams>()
const { data, loading, error } = useGetPostQuery({
variables: { id: id },
})
if (loading) return <Loader active />
if (error) {
return (
<Container text className="mt-24">
<Header as="h1">Ouch! That page didn't load</Header>
<p>Here's why : {error.message}</p>
</Container>
)
}
if (!data?.getPost) {
return (
<Container text className="mt-24">
<Header as="h1">This is not a post</Header>
<p>You've navigated to a post that doesn't exist.</p>
<p>That most likely means that the id {id} isn't the id of post.</p>
</Container>
)
}
let dateStr = "at some unknown time"
if (data.getPost.datePublished) {
dateStr =
DateTime.fromISO(data.getPost.datePublished).toRelative() ?? dateStr
}
const paras = data.getPost.text.split("\n").map((str) => (
<p key={str}>
{str}
<br />
</p>
))
const comments = (
<div className="mt-3">
{data.getPost.comments?.map((comment) => {
return (
<Comment.Group key={comment.id}>
<Comment>
<Comment.Avatar
src={avatar(comment.author.avatarImg)}
size="mini"
/>
<Comment.Content>
<Comment.Author as="a">
{comment.author.displayName}
</Comment.Author>
<Comment.Text>{comment.text}</Comment.Text>
</Comment.Content>
</Comment>
</Comment.Group>
)
})}
</div>
)
return (
<div className="layout-margin">
<div>
<Header as="h1">{data.getPost.title} </Header>
<span className="ui red empty mini circular label"></span>
{" " + data.getPost?.category.name + " "}
{data.getPost?.tags
?.trim()
.split(/\s+/)
.map((tag) => {
if (tag !== "") {
return (
<Label as="a" basic color="grey" key={tag}>
{tag}
</Label>
)
}
})}
</div>
<Header as="h4" image>
<Image
src={avatar(data.getPost?.author.avatarImg)}
rounded
size="mini"
/>
<Header.Content>
{data.getPost?.author.displayName}
<Header.Subheader>{dateStr}</Header.Subheader>
</Header.Content>
</Header>
{paras}
{comments}
</div>
)
}
Now you can click on a post from the home screen and navigate to its page.
This Step in GitHub
This step is also available in the tutorial GitHub repo with the graphql-queries tag and is this code diff.
If you have the app running (yarn start
) you can navigate to http://localhost:3000
to see the post list on the home screen and click on a post’s title to navigate to the post’s page. In the diff, we’ve added a little extra, like the Diggy logo, that’s also a link to navigate you home.