AWS AppSync App with React and Apollo
Introduction
Walk through on how to build an application using AWS AppSync and what all things are needed on the front end side to get the whole system working. We will not be covering any basics as there are so many blog posts out there for that.
Prerequisites
Basic understanding of:
- React
- React router
- AWS AppSync setup inside AWS console
- Different Apollo libraries
- GraphQL terminologies - Query, Mutations and Subscriptions
Resources
If you don’t have basic understanding on any of these topics then I would highly recommended learning about that before proceeding ahead.
What are we going to learn
- Wire up AWS AppSync with all the config
- Authentication using AWS Cognito integration
- Manage mutation and resolvers
- Manage client state and server side state using apollo
Motivations
- Not able to find any docs which explains how all these pieces work together.
- Not much info for advanced level stuff vs basic getting started.
- Spent so much time trying to figure out all these pieces and how they work together.
Setup
Libraries
aws-amplify
,aws-amplify-react
- For Auth and other AWS callsaws-appsync
,aws-appsync-react
- Wrapper around apollo client which manages communication tographql
api endpointreact-apollo
- Apollo client side library for React framework- Other apollo libraries like
apollo-link-http
,apollo-link-state
andapollo-cache-inmemory
Note: Most of the blogs out their doesn’t cover all the aspects of an app and that why they only use sub-set of these libraries.
Building blocks
Basic app loading
I assume you already have something loading just using react
(create using create-react-app
). We are going to build on top of that. So let’s say you have a div rendering with hello world for now.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
class App extends React.Component {
render() {
return <div>Hello World</div>;
}
}
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
Add AWS AppSync layer to connect to backend
We are going to use aws-amplify
library provided by AWS AppSync team. This would take care of talking directly to AWS resources like cognito
for Auth, analytics api, pubsub, API calls, etc. For more detailed info, please use the readme.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import Amplify, { Auth } from "aws-amplify";
Amplify.configure({
Auth: {
// REQUIRED - Amazon Cognito Identity Pool ID
identityPoolId: "XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab",
// REQUIRED - Amazon Cognito Region
region: "XX-XXXX-X",
// OPTIONAL - Amazon Cognito User Pool ID
userPoolId: "XX-XXXX-X_abcd1234",
// OPTIONAL - Amazon Cognito Web Client ID
userPoolWebClientId: "XX-XXXX-X_abcd1234"
}
});
class App extends React.Component {
render() {
return <div>Hello World</div>;
}
}
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
- Once you have configured
Amplify
then in any other module you can import a particular artifact and use it.- Docs - https://aws.github.io/aws-amplify/media/developer_guide
- In this case we are only using Auth so we have configured that portion only. But for API or any other modules, you can add a block and its configurations.
- As per the above setup you should be good to go and use
Auth
module anywhere in your app.
Add cache and state management
We are going to use aws-appsync
and aws-appsync-react
libraries to create local cache where the data from graphql and your local state will be saved. The concept is more like Redux
but here behind the scene aws-appsync
uses Apollo cache and its libraries to do the heavy lifting.
import * as React from "react";
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import Amplify, { Auth } from "aws-amplify";
import { ApolloProvider } from "react-apollo";
import { ApolloLink } from "apollo-link";
import { InMemoryCache } from "apollo-cache-inmemory";
import AWSAppSyncClient, { createAppSyncLink } from "aws-appsync";
import { withClientState } from "apollo-link-state";
Amplify.configure({
Auth: {
// REQUIRED - Amazon Cognito Identity Pool ID
identityPoolId: "XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab",
// REQUIRED - Amazon Cognito Region
region: "XX-XXXX-X",
// OPTIONAL - Amazon Cognito User Pool ID
userPoolId: "XX-XXXX-X_abcd1234",
// OPTIONAL - Amazon Cognito Web Client ID
userPoolWebClientId: "XX-XXXX-X_abcd1234"
}
});
const cache = new InMemoryCache();
let nextTodoId = 1;
const stateLink = withClientState({
cache,
defaults: {
todos: []
},
resolvers: {
Mutation: {
addTodo: (_, { text }, { cache }) => {
const query = gql`
query GetTodos {
todos @client {
id
text
completed
}
}
`;
const previous = cache.readQuery({ query });
const newTodo = {
id: nextTodoId++,
text,
completed: false,
__typename: "TodoItem"
};
const data = {
todos: previous.todos.concat([newTodo])
};
cache.writeData({ data });
return newTodo;
}
}
}
});
const authConfig = {
type: appSyncAtrributes.authenticationType,
jwtToken: async () =>
(await Auth.currentSession()).getAccessToken().getJwtToken()
};
const client = new AWSAppSyncClient(
{
disableOffline: true,
url: appSyncAtrributes.graphqlEndpoint,
region: appSyncAtrributes.region,
auth: authConfig,
complexObjectsCredentials: () => Auth.currentCredentials()
},
{
cache,
link: ApolloLink.from([
stateLink,
createAppSyncLink({
url: appSyncAtrributes.graphqlEndpoint,
region: appSyncAtrributes.region,
auth: authConfig,
complexObjectsCredentials: () => Auth.currentCredentials()
})
])
}
);
class App extends React.Component {
render() {
return <div>Hello World</div>;
}
}
ReactDOM.render(
<BrowserRouter>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</BrowserRouter>,
document.getElementById("root")
);
A lot of stuff happened in the code above. Lets discuss the important pieces here.
Cache/State
Based on all the articles out their, when you use AWSAppSyncClient
directly by providing the first parameter you automagically start maintaining local cache of your remote calls. i.e. when you fetch data from your graphql api then its stored inside cache. But we want to maintain some client side local state as well, which is not coming from the grapghql api. We do that using apollo-link-state
.
Most important pieces to understand here are:
createAppSyncLink
- Default state setup done by theaws-appsync
library.withClientState
- we create the local state maintained by the app on the front-end. In this case it’s the todo state.ApolloLink.from
- Using this we combine the output from above 2 commands to get a single entry point into the state. Think of this as merging the remote and local state into a single state.ApolloProvider
- It’s likereact-redux
provider which exposes the client downstream into other components.
Using state in component
Here I am just going to focus on the App
component as all the other code around it remains the same. Ideally you should create the App
component as a separate file and import it.
import * as React from "react";
import { Mutation, Query } from "react-apollo";
import gql from "graphql-tag";
const GET_TODOS = gql`
{
todos @client {
id
completed
text
}
}
`;
const GET_ORDERS = gql`
{
listOrders {
items {
id
name
}
}
}
`;
const ADD_TODO = gql`
mutation addTodo($text: String!) {
addTodo(text: $text) @client {
id
}
}
`;
class App extends React.Component {
render() {
return (
<Query query={GET_ORDERS}>
{({ data }) => (
<Mutation mutation={ADD_TODO}>
{(addTodo) => (
<>
<div>{JSON.stringify(data)}</div>
<button
onClick={() =>
addTodo({
variables: { text: "gyandeep" }
})
}
>
add
</button>
</>
)}
</Mutation>
)}
</Query>
);
}
}
Most important pieces to understand here are:
Query
andMutation
- Components for graphql stuff.onClick
addTodo
call - Add todo calls a mutation on the client side using the directive@client
inside the mutation definition. This tells the underlying apollo infrastructure that this graphql command is for local changes only.- In the main file in
stateLink
usingwithClientState
, we defined the mutation resolver for add todo that basically writes to the local cache and then the components refreshes to read the values. - Think of this as
redux
actions and reducers.
- In the main file in
GET_ORDERS
- This graphql query doesn’t use the@client
so it hit’s the graphql interface over the network and then when data comes back it updates the cache automagically.
Authentication
After all the setup done, if you want your route in this case App
to be authenticated before procedding then you can use the helper to achieve that.
import { withAuthenticator } from "aws-amplify-react";
// App class definition here as seen above
export default withAuthenticator(App);
So before this components gets rendered it will route to login page. For more details on this I would recommend using the guide here. It explains everything very nicely.
Conclusion
- You should not use
Redux
as it doesn’t work nicely with apollo cache. I know its hard but once you move over to apollo you will be fine.- It took me some time to understand.
- Think of your resolvers as redux reducers, I would recommend writing them separately.
- Think of all
gql
queries as actions, I would recommend writing them separately as well.
Notes
- Wrote this because I struggled with all these pieces (different libraries) a lot and complained a lot to people and twitter.
- I am just 2-3 weeks old in AppSync world, so this post is like a brain dump.
Leave a comment