Create an Open Source Twitter Clone with Apollo, React and Crowdbotics

2019-10-15

In the last few years, GraphQL has become a popular choice to build an API. It also serves a great equivalent and alternative to REST approach.

GraphQL is an open source query language for developing APIs. In contrast to REST, which is an architecture or 'a way of doing things', GraphQL was developed with the concept that a client requests only the desired set of items from the server in a single request.

In this tutorial, you will be building a bare minimum Twitter clone (front-end) with Apollo Client and React application.

To follow this tutorial, you are going to need a server which serves the data from the MongoDB database via a GraphQL API. Luckily, if you haven't read the previous post, you can still clone this Github Repository, follow some instructions to start the server and follow along with this tutorial. Also, make sure you have MongoDB installed on your local dev environment, or if you know how to deploy a MongoDB instance on the cloud, go ahead and use that.

Contents

  • Requirements
  • Getting Started: Create a New React Project
  • Integrate Apollo Client
  • Create the Tweets Component
  • Creating GraphQL Queries
  • Creating GraphQL Mutations
  • Display All Tweets
  • Creating a new Tweet
  • Connecting Crowdbotics support to Your Github Repo
  • Conclusion

Requirements

  • Nodejs 8.x.x or higher installed along with npm/yarn
  • create-react-app global module to scaffold a React project
  • The server-side app up and running that serves the GraphQL API for the React Client

Bonus: You can now use npx to generate a new React project without installing create-react-app.

Getting Started: Create a New React Project

To create a new React project, make sure you have create-react-app installed as a global module. Run the following command, to create a new project.

1create-react-app twitter-clone-apollo-client
2
3# traverse inside the project dir
4cd twitter-clone-apollo-client

You can name your React project anything at the moment. After it is created, traverse inside it and to test or verify if everything is running correctly, start the development server with the following command.

1npm start

This will open a new browser window at the URL http://localhost:3000 with the default app. Also, for this tutorial, I am currently using create-react-appversion 3. You need at least this version or greater in order to follow along.

Integrate Apollo Client

Apollo is a team that builds and maintain a toolchain of GraphQL tools for various use cases like frontend (client), server and engine. There different ways to use or integrate Apollo in your app. In the Twitter Clone Server tutorial, we learned that you can use Apollo on a server-side application to query data and create a GraphQL API.

The Apollo Client helps you use a GraphQL API on the frontend side of an application. Using Apollo Client you can query the API in two ways, whether you have your own server or a third party GraphQL API. It integrates very well with popular frontend frameworks like React, Angular, Vue and so on.

How can you use Apollo in a React app?

To use Apollo, you will need to install dependencies that will be required in order to hook Apollo in the React app. Install the following dependencies either using npm or yarn. I will be using yarn since it is the default package manager for any React project.

1yarn add apollo-boost graphql react-apollo

Briefly, what do these dependencies do?

  • apollo-boost is the package that contains everything that you need to set up an Apollo Client.
  • graphql is required to parse the GraphQL queries.
  • react-apollo is the Apollo integration for React.

In order to proceed, make sure you have the MongoDB instance running on your local dev environment. You can bootstrap one using the command mongod from the terminal. Also, make sure that the Twitter clone server is also running.

Now, open up the file src/index.js. You will modify this file in order to connect the backend endpoint to ApolloClient. This ApolloClient will later help us to build a UI comfortably by fetching the data from the GraphQL QPI. You are also going to wrap App component with ApolloProvider that will in return allow us to access the context of the ApolloClient anywhere in this React app.

1import React from 'react'
2import ReactDOM from 'react-dom'
3import ApolloClient from 'apollo-boost'
4import { ApolloProvider } from 'react-apollo'
5
6import './index.css'
7import App from './App'
8import * as serviceWorker from './serviceWorker'
9
10const client = new ApolloClient({
11 uri: 'http://localhost:5000/graphiql'
12})
13
14const AppContainer = () => (
15 <ApolloProvider client={client}>
16 <App />
17 </ApolloProvider>
18)
19
20ReactDOM.render(<AppContainer />, document.getElementById('root'))
21
22// If you want your app to work offline and load faster, you can change
23// unregister() to register() below. Note this comes with some pitfalls.
24// Learn more about service workers: https://bit.ly/CRA-PWA
25serviceWorker.unregister()

The ApolloClient is imported from the apollo-boost library and the ApolloProvider is imported from the react-apollo library. It is always recommended to put the wrapper like ApolloProvider somewhere high in the component tree of your React app. The reason being is that you need to make sure that all components in the component tree are able to fetch data from the GraphQL API.

In most cases, you are going to end up wrapping App component inside the ApolloProvider. In the above snippet, client is the endpoint that will allow you to fetch data from the API.

Create the Tweets Component

Let us now create a new component components/Tweets.js like below.

1import React from 'react'
2
3class Tweets extends React.Component {
4 render() {
5 return (
6 <div>
7 <h1>Twitter Clone</h1>
8 </div>
9 )
10 }
11}
12
13export default Tweets

Modify the App.js and import the newly created Tweets component.

1import React from 'react'
2import Tweets from './components/Tweets'
3
4function App() {
5 return <Tweets />
6}
7
8export default App

Notice, in the above snippet, App is a functional component. This is create-react-app version 3. On running yarn start you will get the following result.

ss1

Right now it does not look good. Let us add some styling a skeleton component of what things are going to look like. To add styling, create a new file inside the components/ directory called Tweets.css.

1body {
2 background-color: #e6ecf0;
3}
4
5.tweet {
6 margin: 20px auto;
7 padding: 20px;
8 border: 1px solid #ccc;
9 height: 150px;
10 width: 80%;
11 position: relative;
12 background-color: #ffffff;
13}
14
15.author {
16 text-align: left;
17 margin-bottom: 20px;
18}
19
20.author strong {
21 position: absolute;
22 top: 40px;
23 margin-left: 10px;
24}
25
26.author img {
27 width: 50px;
28 height: 50px;
29 border-radius: 50%;
30}
31
32.content {
33 text-align: left;
34 color: #222;
35 text-align: justify;
36 line-height: 25px;
37}
38
39.date {
40 color: #aaa;
41 font-size: 14px;
42 position: absolute;
43 bottom: 10px;
44}
45
46.twitter-logo img {
47 position: absolute;
48 right: 10px;
49 top: 10px;
50 width: 20px;
51}
52
53.createTweet {
54 margin: 20px auto;
55 background-color: #f5f5f5;
56 width: 86%;
57 height: 225px;
58 border: 1px solid #aaa;
59}
60
61.createTweet header {
62 color: white;
63 font-weight: bold;
64 background-color: #2aa3ef;
65 border-bottom: 1px solid #aaa;
66 padding: 20px;
67}
68
69.createTweet section {
70 padding: 20px;
71 display: flex;
72}
73
74.createTweet section img {
75 border-radius: 50%;
76 margin: 10px;
77 height: 50px;
78}
79
80textarea {
81 border: 1px solid #ddd;
82 height: 80px;
83 width: 100%;
84}
85
86.publish {
87 margin-bottom: 20px;
88}
89
90.publish button {
91 cursor: pointer;
92 border: 1px solid #2aa3ef;
93 background-color: #2aa3ef;
94 padding: 10px 20px;
95 color: white;
96 border-radius: 20px;
97 float: right;
98 margin-right: 20px;
99}
100
101.delete {
102 position: absolute;
103 right: 10px;
104 bottom: 10px;
105 cursor: pointer;
106}
107
108.edit {
109 position: absolute;
110 right: 30px;
111 bottom: 10px;
112 cursor: pointer;
113}

Now, edit the file Tweets.js as the following snippet.

1import React from 'react'
2import './Tweets.css'
3import TwitterLogo from '../assets/twitter.svg'
4
5class Tweets extends React.Component {
6 render() {
7 return (
8 <div className="tweets">
9 <div className="tweet">
10 <div className="author">
11 <img
12 src={'https://api.adorable.io/avatars/190/abott@adorable.png'}
13 alt="user-avatar"
14 />
15 <strong>@amanhimself</strong>
16 </div>
17 <div className="content">
18 <div className="twitter-logo">
19 <img src={TwitterLogo} alt="twitter-logo" />
20 </div>
21 <textarea autoFocus className="editTextarea" value="" onChange="" />
22 </div>
23 </div>
24 </div>
25 )
26 }
27}
28
29export default Tweets

It is nothing a but a simple box with static user image, twitter logo and a text area for now. You can find the TwitterLogo inside the src/assets with this project's Github repository. In the browser window, you will get the following result.

ss2

Creating GraphQL Queries

In this section, you are going to write queries and mutations in order to fetch the data when communicating with GraphQL API. To get started, create a new directory inside the src/ directory and name it graphql/. This directory will have two further sub-directories, one for each mutations and queries. Both of these sub-directories will have a file called index.js. In short, here is the new project structure is going to look like.

ss3

First, let us create a query. Open up queries/index.js file and add the following.

1import { gql } from 'apollo-boost'
2
3export const QUERY_GET_TWEETS = gql`
4 query getTweets {
5 getTweets {
6 _id
7 tweet
8 author
9 createdAt
10 }
11 }
12`

The above snippet will be responsible for making a request to the Graphql API. In return, it wants all the tweets stored in the database, hence the name getTweets. The query itself in written inside the string templates. The gql tag parses this query string into an AST. It makes it easier to differentiate a graphql string like in the above snippet from normal JavaScript string templates.

To fetch tweets create a new component called Query.js. This component will utilize the helper component known as Query that comes with react-apollo. This component accepts props from the graphQL query and tells React what to render. It has three pre-defined properties that can be leveraged: loading, error and data in order to render. Depending on the state of the query one of them will be rendered.

1import React, { Component } from 'react'
2import { Query as ApolloQuery } from 'react-apollo'
3
4class Query extends Component {
5 render() {
6 const { query, render: Component } = this.props
7
8 return (
9 <ApolloQuery query={query}>
10 {({ loading, error, data }) => {
11 if (loading) {
12 return <p>Loading</p>
13 }
14 if (error) {
15 return <p>{error}</p>
16 }
17 return <Component data={data || false} />
18 }}
19 </ApolloQuery>
20 )
21 }
22}
23
24export default Query

Creating GraphQL Mutations

The mutations will be following a similar pattern as the query we built in the previous section. Open graphql/mutations/index.js file and add two mutations as below.

1import { gql } from 'apollo-boost'
2
3export const MUTATION_CREATE_TWEET = gql`
4 mutation createTweet($tweet: String, $author: String) {
5 createTweet(tweet: $tweet, author: $author) {
6 _id
7 tweet
8 author
9 }
10 }
11`
12
13export const MUTATION_DELETE_TWEET = gql`
14 mutation deleteTweet($_id: String) {
15 deleteTweet(_id: $_id) {
16 _id
17 tweet
18 author
19 }
20 }
21`

The first mutation is to create a new tweet with the tweet and the author of the tweet, both represented by the scalar type string. In return, you are getting the newly created tweet's id, tweet, and the author fields. The second mutation is to delete the tweet itself. An _id has to be provided in order to delete a tweet from the database.

Now, let us create a component to run these mutations. Again, we are going to leverage the helper component from react-apollo called Mutation. Create a new component file, Mutation.js. This is going to be a long component so let us break it into parts. Start by importing the helper function.

1import React, { Component } from 'react'
2import { Mutation as ApolloMutation } from 'react-apollo'
3
4class Mutation extends Component {
5 // ...
6}
7
8export default Mutation

Define the incoming props inside the render function. The Mutation component in the above snippet accept a different number of props. For our use case, we are interested in the following.

  • mutation: This is a required prop by the helper component. It parses a GraphQL mutation document into an AST using gql string templates.
  • query: It parses a GraphQL query into an AST using gql string templates.
  • children: Another required prop. It is a function that allows triggering a mutation from the UI.
  • onCompleted: This is a callback that executes once the mutation successfully completes.
1render() {
2 const {
3 mutation,
4 query,
5 children,
6 onCompleted
7 } = this.props;
8
9 return(
10 <ApolloMutation>
11 {*/... */}
12 </ApolloMutation>
13 )
14}

Next, return the helper component to render. It will accept the usual props such from above such as mutation. Next, it will utilize a function as a prop to update the cache after a mutation occurs. This function is again available to our ApolloMutation helper component. Further, you responsible to get the name of the query and mutation which both are being received as props to helper component and then get the cached data from the previous query.

1<ApolloMutation
2 mutation={mutation}
3 update={(cache, { data }) => {
4 const { definitions: [{ name: { value: mutationName } }] } = mutation;
5 const { definitions: [{ name: { value: queryName } }] } = query;
6 const cachedData = cache.readQuery({ query });
7 const current = data[mutationName];
8>

In the above snippet, we consume the state of the current data from mutation prop. Then, define a new array that will contain the state of the updated data in case of a new mutation happens. Converting a mutation name in lower case helpful to run a series of if statements to check whether the mutation is being deleted or created.

1let updatedData = []
2const mutationNameLC = mutationName.toLowerCase()
3
4if (mutationNameLC.includes('delete') || mutationNameLC.includes('remove')) {
5 updatedData = cachedData[queryName].filter(row => row._id !== current._id)
6} else if (
7 mutationNameLC.includes('create') ||
8 mutationNameLC.includes('add')
9) {
10 updatedData = [current, ...cachedData[queryName]]
11}

Lastly, update the data to refresh the list of tweets. Then, render the content of the component but before it, use the onCompleted method as a prop such that when a mutation to delete or create a new tweet completes, it triggers the method onCompleted.

1<ApolloMutation
2 // ...
3 cache.writeQuery({
4 query,
5 data: {
6 [queryName]: updatedData
7 }
8 });
9 }} // update prop ends here
10 onCompleted={onCompleted}
11>
12 {children}
13</ApolloMutation>

The complete code for ApolloMutation component looks like below.

1import React, { Component } from 'react'
2import { Mutation as ApolloMutation } from 'react-apollo'
3
4class Mutation extends Component {
5 render() {
6 const { mutation, query, children, onCompleted } = this.props
7
8 return (
9 <ApolloMutation
10 mutation={mutation}
11 update={(cache, { data }) => {
12 const {
13 definitions: [
14 {
15 name: { value: mutationName }
16 }
17 ]
18 } = mutation
19 const {
20 definitions: [
21 {
22 name: { value: queryName }
23 }
24 ]
25 } = query
26 const cachedData = cache.readQuery({ query })
27 const current = data[mutationName]
28 let updatedData = []
29 const mutationNameLC = mutationName.toLowerCase()
30
31 if (
32 mutationNameLC.includes('delete') ||
33 mutationNameLC.includes('remove')
34 ) {
35 updatedData = cachedData[queryName].filter(
36 row => row._id !== current._id
37 )
38 } else if (
39 mutationNameLC.includes('create') ||
40 mutationNameLC.includes('add')
41 ) {
42 updatedData = [current, ...cachedData[queryName]]
43 }
44 cache.writeQuery({
45 query,
46 data: {
47 [queryName]: updatedData
48 }
49 })
50 }}
51 onCompleted={onCompleted}
52 >
53 {children}
54 </ApolloMutation>
55 )
56 }
57}
58
59export default Mutation

Display All Tweets

Since both of the helper components are now wind up, to proceed new to create a Tweet component that will handle mutations to create and delete a new tweet. Create a file called Tweet.js inside the components directory. Again, there is a lot going on in this component. So let us break it down into understandable parts. Later, in this section, you will get the complete code for the component.

We start by importing the necessary that includes GraphQL mutations and the query and the Mutation helper component. Then, we are importing assets like TwitterLogo and a placeholder image for the user's avatar.

1import React, { Component } from 'react'
2import Mutation from './Mutation'
3import {
4 MUTATION_DELETE_TWEET,
5 MUTATION_UPDATE_TWEET
6} from '../graphql/mutations'
7import { QUERY_GET_TWEETS } from '../graphql/queries'
8import TwitterLogo from '../assets/twitter.svg'
9
10const Avatar = 'https://api.adorable.io/avatars/190/abott@adorable.png'

Inside the Tweet component there is a function to delete the tweet by running the required mutation.

1handleDeleteTweet = (mutation, _id) => {
2 mutation({
3 variables: {
4 _id
5 }
6 })
7 }
8}

Next, inside the render function, map all the existing tweets and display them and then make use of Mutation component.

1render() {
2 const {
3 data: { getTweets: tweets }
4 } = this.props
5
6 return tweets.map(({ _id, tweet, author }) => (
7 <div className='tweet' key={`tweet-${_id}`}>
8 <div className='author'>
9 <img src={Avatar} alt='avatar' />
10 <strong>{author}</strong>
11 </div>
12
13 <div className='content'>
14 <div className='twitter-logo'>
15 <img src={TwitterLogo} alt='Twitter' />
16 </div>
17 {tweet}
18 </div>
19 <Mutation mutation={MUTATION_DELETE_TWEET} query={QUERY_GET_TWEETS}>
20 {deleteTweet => (
21 <div
22 className='delete'
23 onClick={() => {
24 this.handleDeleteTweet(deleteTweet, _id)
25 }}
26 >
27 <span>Delete Tweet</span>
28 </div>
29 )}
30 </Mutation>
31 ))
32 }

Here is the complete code for Tweet.js file.

1import React, { Component } from 'react'
2import Mutation from './Mutation'
3import { MUTATION_DELETE_TWEET } from '../graphql/mutations'
4import { QUERY_GET_TWEETS } from '../graphql/queries'
5import TwitterLogo from '../assets/twitter.svg'
6
7const Avatar = 'https://api.adorable.io/avatars/190/abott@adorable.png'
8
9class Tweet extends Component {
10 handleDeleteTweet = (mutation, _id) => {
11 mutation({
12 variables: {
13 _id
14 }
15 })
16 }
17
18 render() {
19 const {
20 data: { getTweets: tweets }
21 } = this.props
22
23 return tweets.map(({ _id, tweet, author }) => (
24 <div className="tweet" key={`tweet-${_id}`}>
25 <div className="author">
26 <img src={Avatar} alt="avatar" />
27 <strong>{author}</strong>
28 </div>
29
30 <div className="content">
31 <div className="twitter-logo">
32 <img src={TwitterLogo} alt="Twitter" />
33 </div>
34 {tweet}
35 </div>
36 <Mutation mutation={MUTATION_DELETE_TWEET} query={QUERY_GET_TWEETS}>
37 {deleteTweet => (
38 <div
39 className="delete"
40 onClick={() => {
41 this.handleDeleteTweet(deleteTweet, _id)
42 }}
43 >
44 <span>Delete Tweet</span>
45 </div>
46 )}
47 </Mutation>
48 </div>
49 ))
50 }
51}
52
53export default Tweet

We haven't created the functionality that adds a new tweet yet but I have added two tweets manually to verify that the Tweet function is working properly. Modify the Tweets.js file like below to fetch all the existing tweets from the database.

1import React from 'react'
2import Tweet from './Tweet'
3import Query from './Query'
4import { QUERY_GET_TWEETS } from '../graphql/queries'
5import './Tweets.css'
6import TwitterLogo from '../assets/twitter.svg'
7
8class Tweets extends React.Component {
9 render() {
10 return (
11 <div className="tweets">
12 <div className="tweet">
13 <div className="author">
14 <img
15 src={'https://api.adorable.io/avatars/190/abott@adorable.png'}
16 alt="user-avatar"
17 />
18 <strong>@amanhimself</strong>
19 </div>
20 <div className="content">
21 <div className="twitter-logo">
22 <img src={TwitterLogo} alt="twitter-logo" />
23 </div>
24 <textarea autoFocus className="editTextarea" value="" onChange="" />
25 </div>
26 </div>
27 <Query query={QUERY_GET_TWEETS} render={Tweet} />
28 </div>
29 )
30 }
31}
32
33export default Tweets

If you add the one or two tweets manually, you will get the following result.

ss4

Creating a new Tweet

In this section, let us create a new component called CreateTweet to pursue the functionality of adding a new tweet to the database. As usual, start by importing the necessary files as below.

1// Dependencies
2import React, { Component } from 'react'
3import Mutation from './Mutation'
4import { MUTATION_CREATE_TWEET } from '../graphql/mutations'
5import { QUERY_GET_TWEETS } from '../graphql/queries'
6const Avatar = 'https://api.adorable.io/avatars/190/abott@adorable.png'
7
8class CreateTweet extends Component {
9 state = {
10 tweet: ''
11 }
12
13 handleChange = e => {
14 const {
15 target: { value }
16 } = e
17
18 this.setState({
19 tweet: value
20 })
21 }
22
23 handleSubmit = mutation => {
24 const tweet = this.state.tweet
25 const author = '@amanhimself'
26
27 mutation({
28 variables: {
29 tweet,
30 author
31 }
32 })
33 }
34
35 render() {
36 return (
37 <Mutation
38 mutation={MUTATION_CREATE_TWEET}
39 query={QUERY_GET_TWEETS}
40 onCompleted={() => {
41 this.setState({
42 tweet: ''
43 })
44 }}
45 >
46 {createTweet => (
47 <div className="createTweet">
48 <header>Write a new Tweet</header>
49
50 <section>
51 <img src={Avatar} alt="avatar" />
52
53 <textarea
54 placeholder="Write your tweet here..."
55 value={this.state.tweet}
56 onChange={this.handleChange}
57 />
58 </section>
59
60 <div className="publish">
61 <button
62 onClick={() => {
63 this.handleSubmit(createTweet)
64 }}
65 >
66 Tweet
67 </button>
68 </div>
69 </div>
70 )}
71 </Mutation>
72 )
73 }
74}
75
76export default CreateTweet

Define a local state that will store the creation of the new tweet and will get an update as soon as there is a change in the textarea. The handleChange listens to any changes in the input value of the textarea and then updates the tweet variable from the state. To execute the mutation MUTATION_CREATE_TWEET when the user clicks the button Tweet, the method handleSubmit is responsible.

Add this component to Tweets.js file as below.

1import React from 'react'
2import Tweet from './Tweet'
3import CreateTweet from './CreateTweet'
4import Query from './Query'
5import { QUERY_GET_TWEETS } from '../graphql/queries'
6import './Tweets.css'
7
8class Tweets extends React.Component {
9 render() {
10 return (
11 <div className="tweets">
12 <CreateTweet />
13 <Query query={QUERY_GET_TWEETS} render={Tweet} />
14 </div>
15 )
16 }
17}
18
19export default Tweets

To add a new tweet, try writing something and then hit the Tweet button. You will get similar results.

ss5

Conclusion

By the end of this tutorial, you have learned:

  • how to integrate an ApolloClient in a React app
  • use GraphQL query and mutations to receive and send data to the API
  • to utilize helper functions such as Mutation and Query from the react-apollo library
  • also, understand each helper functions' props
  • understand the reason behind why to wrap the App component with ApolloProvider
  • how to use gql template literals in order to create a query or a mutation

To learn more about react-apollo library, you can visit the official API documentation here.

You can find the complete code for this post at this Github Repository.

Originally published at Crowdbotics.com

I'm Aman working as an independent fullstack developer with technologies such as Node.js, ReactJS, and React Native. I try to document and write tutorials to help JavaScript, Web and Mobile developers.