Getting Started

The first thing I did was created a new React application:

npx create-react-app write-with-me

The next thing I needed to do was to find the tool I was going to use to allow markdown in a React App. I stumbled upon react-markdown by Espen Hovlandsdal (rexxars on Twitter).

npm install react-markdown

React Markdown was super simple to use. You import ReactMarkdown from the package & pass in the markdown you’d like to render as a prop:

const ReactMarkdown = require('react-markdown')



const input = '# This is a header



And this is a paragraph'



<ReactMarkdown source={input} />

Building the API

Now that we have the markdown tool the next set was creating the API. I used AWS AppSync & AWS Amplify to do this. With the Amplify CLI, you can set a base type & add a decorator to build out the entire backend by taking advantage of the GraphQL Transform library.

amplify init amplify add api // chose GraphQL

// Choose API Key as the authentication

The schema I used was this:



id: ID!

clientId: ID!

markdown: String!

title: String!

createdAt: String

} type Post @model id: ID!clientId: ID!markdown: String!title: String!createdAt: String

The @model decorator will tell Amplify to deploy a GraphQL backend complete with a DynamoDB data source & schema (types & operations) for all crud operations & GraphQL subscriptions. It will also create the resolvers for the created GraphQL operations.

After defining & saving the schema we deploy the AppSync API:

amplify push

When we run amplify push , we’re also given the option to execute GraphQL codegen in either TypeScript, JavaScript, or Flow. Doing this will introspect your GraphQL schema & automatically generate the GraphQL code you’ll need on the client in order to execute queries, mutations, & subscriptions.

Installing the dependencies

Next, we install the other necessary libraries we need for the app:

npm install aws-amplify react-router-dom uuid glamor debounce

uuid — to create unique IDs to uniquely identify the client

— to create unique IDs to uniquely identify the client react-router-dom — to add routing

— to add routing aws-amplify — to interact with the AppSync API

— to interact with the AppSync API glamor — for styling

— for styling debounce — for adding a debounce when user types

Writing the code

Now that our project is set up & our API has been deployed, we can start writing some code!

The app has three main files:

Router.js — Defines the routes

Posts.js — Fetches & renders the posts

Post.js — Fetches & renders a single post

Setting up navigation was pretty basic. We needed two routes: one for listing all of the posts & one for viewing a single post. I decided to go with the following route scheme:

/post/:id/:title

When someone hits the above route, we have access to everything we need to display a title for the post as well as the ID for us to fetch the post if it is an existing post. All of this info is available directly in the route parameters.

React Hooks with GraphQL

If you’ve ever wondered how to implement hooks into a GraphQL application, I recommend also reading my post Writing Custom Hooks for GraphQL because that’s exactly what we’ll be doing.

Instead of going over all of the code for the 2 components (if you’d like to see the code, click on the links above), I’d like to focus on how we implemented the necessary functionality using GraphQL & hooks.

The main API operations we needed from this app are intended to do the following:

Load all of the posts from the API Subscribe to new posts being created by others Load an individual post Subscribe to changes within an individual post & re-render the component

Let’s take a look at each.

Loading all of the posts from the API

To load the posts I went with a useReducer hook combined with a function call within a useEffect hook. We first define the initial state. When the component renders for the first time we call the API & update the state using the fetchPosts function. This reducer will also handle our subscription:

import { listPosts } from './graphql/queries'

import { API, graphqlOperation } from 'aws-amplify' const initialState = {

posts: [],

loading: true,

error: false

} function reducer(state, action) {

switch (action.type) {

case 'fetchPostsSuccess':

return { ...state, posts: action.posts, loading: false }

case 'addPostFromSubscription':

return { ...state, posts: [ action.post, ...state.posts ] }

case 'fetchPostsError':

return { ...state, loading: false, error: true }

default:

throw new Error();

}

} async function fetchPosts(dispatch) {

try {

const postData = await API.graphql(graphqlOperation(listPosts))

dispatch({

type: 'fetchPostsSuccess',

posts: postData.data.listPosts.items

})

} catch (err) {

console.log('error fetching posts...: ', err)

dispatch({

type: 'fetchPostsError',

})

}

} // in the hook const [postsState, dispatch] = useReducer(reducer, initialState) useEffect(() => {

fetchPosts(dispatch)

}, [])

Subscribing to new posts being created

We already have our state ready to go from the above code, now we just need to subscribe to the changes in the hook. To do that, we use a useEffect hook & subscribe to the onCreatePost subscription:

useEffect(() => {

const subscriber = API.graphql(graphqlOperation(onCreatePost)).subscribe({

next: data => {

const postFromSub = data.value.data.onCreatePost

dispatch({

type: 'addPostFromSubscription',

post: postFromSub

})

}

});

return () => subscriber.unsubscribe()

}, [])

In this hook, we set up a subscription that will fire when a user creates a new post. When the data comes through, we call the dispatch function & pass in the new post data that was returned from the subscription.

Fetching a single post

When a user lands on a route, we can use the data from the route params to identify the post name & ID:

In this route, the ID would be 9999b0bb-63eb-4f5b-9805–23f6c2661478 & the name would be Write with me.

When the component loads, we extract these params & use them.

The first thing we do is attempt to create a new post. If this is successful, we are done. If this post already exists, we are given the data for this post from the API call.

This may seem strange at first, right? Why not try to fetch, & if unsuccessful the create? Well, we want to reduce the total number of API calls. If we attempt to create a new post & the post exists, the API will actually return the data from the existing post allowing us to only make a single API call. This data is available in the errors:

err.errors[0].data

We handle the state in this component using useReducer hook.

// initial state

const post = {

id: params.id,

title: params.title,

clientId: CLIENTID,

markdown: '# Loading...'

} function reducer(state, action) {

switch (action.type) {

case 'updateMarkdown':

return { ...state, markdown: action.markdown, clientId: CLIENTID };

case 'updateTitle':

return { ]...state, title: action.title, clientId: CLIENTID };

case 'updatePost':

return action.post

default:

throw new Error();

}

} async function createNewPost(post, dispatch) {

try {

const postData = await API.graphql(graphqlOperation(createPost, { input: post }))

dispatch({

type: 'updatePost',

post: {

...postData.data.createPost,

clientId: CLIENTID

}

})

} catch(err) {

if (err.errors[0].errorType === "DynamoDB:ConditionalCheckFailedException") {

const existingPost = err.errors[0].data

dispatch({

type: 'updatePost',

post: {

...existingPost,

clientId: CLIENTID

}

})

}

}

} // in the hook, initialize the state

const [postState, dispatch] = useReducer(reducer, post) // fetch post

useEffect(() => {

const post = {

...postState,

markdown: input

}

createNewPost(post, dispatch)

}, [])

Subscribing to a post change

The last thing we need to do is subscribe to changes in a post. To do this, we do two things:

Update the API when the user types (both title or markdown changes) Subscribe to changes then the post is updated

async function updatePost(post) {

try {

await API.graphql(graphqlOperation(UpdatePost, { input: post }))

console.log('post has been updated!')

} catch (err) {

console.log('error:' , err)

}

} function updateMarkdown(e) {

dispatch({

type: 'updateMarkdown',

markdown: e.target.value,

})

const newPost = {

id: post.id,

markdown: e.target.value,

clientId: CLIENTID,

createdAt: post.createdAt,

title: postState.title

}

updatePost(newPost, dispatch)

} function updatePostTitle (e) {

dispatch({

type: 'updateTitle',

title: e.target.value

})

const newPost = {

id: post.id,

markdown: postState.markdown,

clientId: CLIENTID,

createdAt: post.createdAt,

title: e.target.value

}

updatePost(newPost, dispatch)

} useEffect(() => {

const subscriber = API.graphql(graphqlOperation(onUpdatePost, {

id: post.id

})).subscribe({

next: data => {

if (CLIENTID === data.value.data.onUpdatePost.clientId) return

const postFromSub = data.value.data.onUpdatePost

dispatch({

type: 'updatePost',

post: postFromSub

})

}

});

return () => subscriber.unsubscribe()

}, [])

When the user types, we update both the local state as well as the API. In the subscription, we first check to see if the subscription data coming in is from the same Client ID. If it is, we do nothing. If it is from another client, we update the state.

Deploying the app on a custom domain

Now that we’ve built the app, what about deploying it to a custom domain like I did with writewithme.dev?

I did this using GoDaddy, Route53 & the Amplify console.

In the AWS dashboard, go to Route53 & click on Hosted Zones. Choose Create Hosted Zone. From there, enter your domain name & click create.

Be sure to enter your domain name as is, without www. E.g. writewithme.dev

Now in Route53 dashboard you should be given 4 nameservers. In your hosting account, set these custom nameservers in your DNS setting for the domain you’re using.

These nameservers should look something like ns-1355.awsdns-41.org, ns-1625.awsdns-11.co.uk, etc…

Next, in the Amplify Console, click on Get Started under the Deploy section.

Connect your GitHub account & then choose the repo & branch that your project lives in.

This will walk you through deploying the app in the Amplify Console & making it live. Once complete, you should see some information about the build & some screenshots of the app: