I'm a big fan of Amplify. I'm also a big fan of TypeScript. Amplify is not built with TypeScript, and to use it effectively, sometimes you need to give it a little help. This is especially true when it comes to GraphQL. With the advent of hooks, we can create some nice utilities for ourselves that let us leverage the power of TypeScript with our GraphQL queries. Let's see what that looks like.
> I'll be assuming familiarity with React and TypeScript, including the usage of Amplify and GraphQL for this post. There are tons of great resources online. Here's a great set of steps to setup a similar project. Or, you can clone the sample app and run amplify init
at the root of the project to see the final result.
Simple Query
From our sample app we have a list of popular games we want to retrieve. The query looks like this:
popularGames {
id
name
popularity
url
summary
# ommitted for brevity
}
Starting out, this is a great first start to what we're going for. We want to use useEffect
to handle fetching our data. We're using Amplify's graphql
method and passing the GraphQL query from above to the graphqlOperation
method.
React.useEffect(() => {
const fetchGames = async () => {
try {
const response = await API.graphql(graphqlOperation(popularGames));
} catch (error) {
console.log(error);
}
};
fetchGames();
}, []);
The response objects has a data property, which contains our list of games. Here's a couple from the list.
{
"data": {
"popularGames": [
{
"id": "76882",
"name": "Sekiro: Shadows Die Twice",
"popularity": 3954.25
},
{
"id": "114455",
"name": "Pacify",
"popularity": 1472.0
}
]
}
}
We want to display these on our page, so we need to load them into state. With hooks, you accomplish this by creating a useState
declaration and then using the method created for you to load them into state.
const [gameData, setGameData] = React.useState(undefined);
React.useEffect(() => {
const fetchGames = async () => {
try {
const response: any = await API.graphql(graphqlOperation(popularGames));
setGameData(response.data);
} catch (error) {
console.log(error);
}
};
fetchGames();
}, []);
At this point, you could display your games on the page using the gameData
object.
But in TypeScript land, we actually have more problems here. By initializing the state object to undefined, TypeScript can only infer that the value allowed for the gameData
object is undefined, and will give us an error if we try to set our query response using the setGameData
method. Additionally, there is a keyword we have used to get past this problem that bites many a first-time TypeScript developer in the ass.
any
This word will strike fear into the hearts of all who witness it. At least, it should if you want your future self to thank your past self at some point (Thanks, Paul, for this great insight).
We don't want any
s anywhere.
We really want to try our best to give ourselves and our tools as much power as possible.
We can't tell what our data will be right now, which is a problem... but Amplify
can help us.
Types to the Rescue
In our project, if it's not configured already, we need to run amplify configure codegen
. This will setup code generation and walk us through the process of generating types based on our queries. This is super helpful (and as far as I'm concerned, should be the default if a .tsconfig
file is present at the root of the project...).
This gives us a type that we can use for our data. Normally, we could just throw this after the variable and be done with it.
const response: { data: PopularGamesQuery } = await API.graphql(
graphqlOperation(popularGames)
);
We know that response is going to be this type if the operation doesn't fail. However, the types returned by the graphql
function are a mashup of GraphQLResult
and Observable
since this same function is used for both. Until this changes, we're going to let it know what we expect to get back. We can do this by using the as
keyword to tell TypeScript what we know our type will be.
const response = (await API.graphql(graphqlOperation(popularGames))) as {
data: PopularGamesQuery;
};
Now we get the type help that we want. Hovering over the response
object confirms that TypeScript recognizes the response to be an object with a data property of type PopularGamesQuery
.
Games, setState
We'll use the same type to tell our useState
function how we want to utilize our gameData
object.
useState
accepts a generic type parameter that allows us to inform the function about the type we want to use.
const [gameData, setGameData] = React.useState<PopularGamesQuery | undefined>(
undefined
);
Here, we have passed the same PopularGamesQuery
type (as well as the possible undefined value we use to initialize it with). This consoles the TypeScript compiler and we can move forward with displaying our games.
As simple as this is, there are a few boilerplatey things that are staring us in the face. We can extract these things out and create some re-usable code that will make future queries in other components much easier to setup. Custom hooks to the rescue!
Stay tuned for the next post where we do just that!
This post was originally posted on my website. You can see this and other articles like it, as well as what I'm all about. Follow me on @twitter and reach out if you have any questions!
Top comments (3)
Your posts have been extremely helpful in crafting an RFC for React Hooks:
Please share & provide input as you see fit!
Mat, the code seems to be incomplete in Github
...from queryHelpers.tsx
** Missing Dependencies **
import Spinner from "../components/ui/Spinner";
import { ErrorBox } from "../containers/auth/ErrorBox";
import useDeepCompareEffect from "use-deep-compare-effect";
import { UndefinedGQLType } from "../types/utils";
import { notEmpty } from "./common";
import { Observable } from "apollo-link";
Please advise
This is outdated. I will be posting about this soon.