Part 2 of a 2 part series on building a GraphQL MERN Stack (MongoDB-Express-React-Node) with Apollo that performs all four CRUD operations (Create-Read-Update-Delete).
By Steve Carey - 7/26/2019
Part 1 of this tutorial: Build a GraphQL API with Node.js and Apollo-Server
Finished code: Github
In this tutorial we will integrate a React client into our GraphQL API with the help if the Apollo client package. We will build an interface to perform the four CRUD actions on the database. The main focus of this tutorial is on the GraphQL and Apollo portions. If you are new to React I still recommend doing this tutorial. I won't be explaining the basic React concepts like props and state but you'll able to see them in action. Then you can go back and build a tic tac toe game like they do in their introductory tutorial.
You need to have Node and Yarn installed on your computer to use React. We will install and use create-react-app to get us going.
Create-react-app is a Node package developed by Facebook that gives you a starter app out of the box. It includes an already configured Webpack server that bundles all the JavaScript files in the app into a bundle.js file that gets attached into the index.html file at runtime. At runtime, Webpack’s dev server will listen for you to save changes in develepment, and will automatically load your running web page with the changes. This is called hot reloading. Install it as a Node global package:
npm install -g create-react-app
Yarn and npm (Node Package Manager) are both package managers for managing JavaScript dependencies. You can use either, but since React and Yarn were both created by Facebook, they tend to interact tightly together so we will be using Yarn to install packages and run the server. Install it globally if it's not already installed.
npm install -g yarn
We already created a GraphQL API that we will be using as our back end. We could keep the front end and back end code in completely separate locations, but if we are to deploy our application in production as an integrated web application it's easier to keep them together. Use the below command from your API project's root directory to add a directory called client, place a preconfigured React application there, and cd into it.
create-react-app client && cd client
Run the server to make sure it's working:
yarn start
You should see the default Welcome to React app in your browser at localhost:3000.
The create-react-app generator created our package.json file. So now we have two of those. One for our API application and one for our React client. The generator installed the react, react-dom, and react-scripts packages. They will be listed as dependencies in the package.json file.
Before you install the other packages you will be using let's make one change to the package.json file. Add a proxy property set to your API's port. This will automatically be added to the API routes you will call from the React client, so you only have to use the path, not the full URL in your code. That may not seem like much of a benefit since you only have to access one route on a GraphQL API. But doing so will help integrate the client with the API as if it was one application, preventing the cross domain access issues discussed in Part 1 of this tutorial. This makes it easier to deploy the app to a platform like Heroku.
{
"name": "client",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
...
}
Install the additional packages we will need for our React app:
yarn add react-router-dom bootstrap graphql react-apollo apollo-boost
Let's talk about the GraphQL-related packages we installed.
Graphql: GraphQL servers are available in over a dozen different programming languages. The graphql package is its implementation in JavaScript.
Apollo is a software company that provides a number of tools and services around the GraphQL query language. You don't have to use Apollo packages to use GraphQL with React, but it adds some advantages.
Apollo-boost: Apollo Client facilitates working with GraphQL data on the client side. The apollo-boost package installs and configures apollo-client and other apollo packages as its dependencies. This includes managing a central store of data which can either replace or at least compliment the Redux store. Apollo-boost is most commonly paired with React but it can be used with Angular or Vue as well.
While you can use both Apollo and Redux to manage your React client's central store, it is best to use one or the other so you don't have two sources of truth. Apollo provides some useful functionality that you would have to code yourself with Redux. It automatically adds GraphQL query results to the store. You can store local data with Apollo as well.
React-apollo: connects the graphql and apollo-boost libraries with your React code. We will be using ApolloProvider, Query, and Mutation components from the react-apollo library.
If you look at the files and directories generated in the client directory, besides a folder for the node_modules you'll see a "public" and a "src" directory. These are where you add your own files. Right now there are no sub-directories in those folders so things can get pretty cluttered quickly if you don't add some structure. From the client directory you can use these UNIX commands to add and remove the relevant directories and files. Or do it from your text editor if you prefer.
rm README.md rm -rf .git mkdir -p src/components/pages touch src/components/pages/Home.js mkdir src/components/articles touch src/components/articles/ArticleList.js touch src/components/articles/ArticleInfo.js touch src/components/articles/ArticleAdd.js touch src/components/articles/ArticleEdit.js mkdir src/graphql touch src/graphql/articleQueries.js
We removed some unnecessary files including the git repository that create-react-app generated for us. We won't be using git in this tutorials but if we were we would generate a git repository in the project root directory for the API and React client combined. You can leave the .gitignore file though since git allows multiple gitignore files.
We created directories for our components. And we created a directory called graphql with a file called articleQueries. This is just so you can have the API query and mutation code in one location for purposes of this tutorial.
The index.css file is for our app-wide css. If you want to add custom CSS classes that apply to the whole app this is where you would do it. The App.css file is specific to the App.component we are going to fill out. Delete the CSS that's in there now so it doesn't conflict with your app.
/* Remove all the CSS classes */
To add css classes that apply only to a particular component you could create a css file in the same folder as that component and import it in the component(s) that use it.
Bootstrap: For convenience we're using Bootstrap for our styling. We installed the bootstrap package earlier. Now import it to the index.js file.
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './App';
...
If you want to use the JavaScript features of Bootstrap like drop-down menus you can also install the react-bootstrap package, be we won't use those in this tutorial.
This is a Single-Page Application (SPA) with CRUD capabilities. It is single page because you only have one html file sitting in the public folder: public/index.html. It has your standard HTML page structure with one element inside the body, an empty div tag with an id named root <div id="root"></div>
. That is where React renders it's output with JavaScript. There's nothing special about the name "root." It can be any name. We won't make any changes to this file.
In the src/index.js file we import the React library and render our main component. Other than adding the Bootstrap import above, we don't need to make any changes to the src/index.js file. It should look like this.
1. import React from 'react';
1. import ReactDOM from 'react-dom';
3 import 'bootstrap/dist/css/bootstrap.css';
4 import './index.css';
1. import App from './App';
6 import * as serviceWorker from './serviceWorker';
7
2. ReactDOM.render(<App />, document.getElementById('root'));
3. serviceWorker.unregister();
With React you split your user interface into components. Each component returns some JSX which ultimately gets rendered as HTML to the user interface. In our case we are creating the front end to a CRUD application. We need components to correspond with the API's endpoints to perform the CRUD actions.
A few notes on syntax. We can use all the latest JavaScript syntax without worrying about browser compatibility because create-react-app installed Babel which compiles our JavaScript into ES5. We did not add the step of installing Babel in our API, so while we could still use most of the ES6 syntax, there are some features not yet supported by Node like the Import and Export syntax. But we can and will be using those here. Also, in general I am using the declaration syntax for functions rather than arrow functions just to make it explicit when you are returning a value. But there is no reason not to use arrow functions if you prefer it.
The App.js component file holds the structure for our web page. The ArticleList, ArticleInfo, ArticleAdd, and ArticleEdit components represent views that correspond to the Read, Read and Delete, Create, and Update actions respectively. We'll also add a Home component to be our home page. Note that component names must be capitalized. In each file we will build one React component, with the exception of App.js where we build three. We could break it out even further, like a form component that gets imported to the ArticleAdd and ArticleEdit components, but we won't.
Before we build our full CRUD app let's start out with a very simple component called Home. We already created an empty Home.js file. Populate it with the below:
1. import React from 'react';
2
2. function Home() {
4 return (
5 <div className="jumbotron">
6 <h1>Home Page</h1>
7 </div>
8 );
9 }
10
3. export default Home;
Right now this file is just sitting in space not connected to anything. If you want to see it in action you can temporarily open the src/index.js file and change the App elements to Home. Then go to your browser and it should now say "Home Page."
# src/index.js ... import Home from './components/pages/Home'; ... ReactDOM.render(<Home />, document.getElementById('root'));
Undo those index.js file changes back to the way it was (referencing "App").
The term Single-Page App can be a little misleading. While it is indeed a single html page, that doesn't mean you can't have multiple views with changes to the URL. We will use the React Router DOM package that we installed to do just that. And we will also perform the standard CRUD actions in our single page interacting with the API to get articles, post new articles, edit existing articles, and delete articles. Instead of doing those things all on separate HTML pages as you would in an traditional web app, we will do it from our single page sending queries and mutations to the API with the help of the GraphQL and Apollo packages we installed. React will move the data around using JavaScript which can add significant speed over entire page loads from the server.
The central component for our app, generated by create-react-app is called "App." That is what was returning the Welcome to React page we saw in the browser. Replace that with a Navigation bar and a place to render the other components. Here is the code for the App.js file all at once:
import React from 'react';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import { BrowserRouter as Router, Route, NavLink, Switch } from 'react-router-dom';
import Home from './components/pages/Home';
import ArticleList from './components/articles/ArticleList';
import ArticleInfo from './components/articles/ArticleInfo';
import ArticleAdd from './components/articles/ArticleAdd';
import ArticleEdit from './components/articles/ArticleEdit';
const client = new ApolloClient({
uri: '/graphql'
});
function App() {
return (
<ApolloProvider client={client}>
<Router>
<Navigation />
<div className="container">
<Main />
</div>
</Router>
</ApolloProvider>
);
}
function Navigation() {
return(
<nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div className="container">
<ul className="navbar-nav mr-auto">
<li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li>
<li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
</ul>
</div>
</nav>
);
}
function Main() {
return(
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/articles" component={ArticleList} />
<Route exact path="/articles/new" component={ArticleAdd} />
<Route exact path="/articles/:_id" component={ArticleInfo} />
<Route exact path="/articles/:_id/edit" component={ArticleEdit} />
</Switch>
);
}
export default App;
First thing's first. Once you add the code and save the file, go to the browser and make sure you didn't get any errors. We removed the Welcome to React header and logo and replaced it with a navigation bar with two links, and a simple home page jumbotron. There is nothing in the articles link yet but we'll add that shortly.
If you are familiar with React you may notice that we are using functional components rather than class components. That's because we are using Apollo Client to manage our state which it can do in a functional component. The React ecosystem is moving away from class components where they can, so we'll only be using functional components in this app. If you are new to React just bask in the happiness that you don't have to get bogged down with componentDidMount... or was it componentWillMount, no it was componentMightHaveMounted. Whatever, let's move on.
This file actually contains three separate components... App, Navigation, and Main. But most of this code relates to Navigation and the React Router package. React Router is the most popular Routing package. The docs are at reacttraining.com/react-router/web/guides which looks like a third party training site, and it is. But they are the ones who created the React Router package (not Facebook's React team).
Alright, let's break this code down in sections. We'll start with the React import:
import React from 'react';
To create a component we need to import the React library.
Create an instance of the Apollo Client
import ApolloClient from 'apollo-boost'; ... const client = new ApolloClient({ uri: '/graphql' });
Import ApolloClient from the apollo-boost libraries then create an instance of it assigned to variable "client". Pass in an object with a uri property. This is the url to our GraphQL API endpoint. We only put the path here because we added the domain and port as a proxy in the client/package.json file.
The App component:
import { ApolloProvider } from 'react-apollo'; import { BrowserRouter as Router, ... } from 'react-router-dom'; ... function App() { #1 return ( <ApolloProvider client={client}> #2 <Router> #3 <Navigation /> <div className="container"> <Main /> </div> </Router> </ApolloProvider> ); }
The Navigation component:
import { ... NavLink,... } from 'react-router-dom'; ... function Navigation() { #1 return( <nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4"> <div className="container"> <ul className="navbar-nav mr-auto"> <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li> #2a <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li> #2b </ul> </div> </nav> ); }
The Main component:
import { ... Route, ... Switch } from 'react-router-dom'; import Home from './components/pages/Home'; #1 import ArticleList from './components/articles/ArticleList'; import ArticleInfo from './components/articles/ArticleInfo'; import ArticleAdd from './components/articles/ArticleAdd'; import ArticleEdit from './components/articles/ArticleEdit'; ... function Main() { #2 return( <Switch> #3 <Route exact path="/" component={Home} /> #4 <Route exact path="/articles" component={ArticleList} /> <Route exact path="/articles/new" component={ArticleAdd} /> <Route exact path="/articles/:_id" component={ArticleInfo} /> <Route exact path="/articles/:_id/edit" component={ArticleEdit} /> </Switch> );
When we built the GraphQL API, we tested it with queries and mutations to each of our endpoints using the graphql playground (if you used apollo-server) or graphiql tool (if you used express-graphql) in the browser. We'll send those same queries and mutations from our React client. Let's put them all in one file for convenience:
1 import { gql } from 'apollo-boost';
2
3 const GET_ARTICLES = gql`
4 {
5 articles {
6 title
7 content
8 id
9 }
10 }
11 `;
12
13 const GET_ARTICLE = gql`
14 query article($id: ID!) {
15 article(id: $id) {
16 id
17 title
18 content
19 }
20 }
21 `;
22
23 const CREATE_ARTICLE = gql`
24 mutation createArticle($title: String!, $content: String!) {
25 createArticle(articleInput: { title: $title, content: $content }) {
26 title
27 content
28 id
29 }
30 }
31 `;
32
33 const DELETE_ARTICLE = gql`
34 mutation deleteArticle($id: ID!) {
35 deleteArticle(id: $id) {
36 title
37 content
38 id
39 }
40 }
41 `;
42
1. const UPDATE_ARTICLE = gql`
2. mutation updateArticle($id: ID!, $title: String!, $content: String!) {
3. updateArticle(id: $id, articleInput: { title: $title, content: $content }) {
4. id
47 title
48 content
49 }
50 }
51 `;
52
53 export { GET_ARTICLES, GET_ARTICLE, CREATE_ARTICLE, DELETE_ARTICLE, UPDATE_ARTICLE };
We have one query or mutation string per endpoint on our API. I won't go through each of these individually but rather go over the structure using UPDATE_ARTICLE as an example from the inside out.
In our app's navbar is a link to "articles" which loads our ArticleList component. Let's populate it.
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import { Query } from 'react-apollo';
4 import { GET_ARTICLES } from '../../graphql/articleQueries';
5
6 function ArticleList() {
7 return(
8 <div>
9 <h2>
10 Articles
1. <Link to="/articles/new" className="btn btn-primary float-right">
12 Create Article
13 </Link>
14 </h2>
2. <Query query={GET_ARTICLES}>
3. {function({ loading, error, data }) {
17 if (loading) return "Loading...";
18 if (error) return `Error! ${error.message}`;
4. const { articles } = data;
20 return (
5. articles.map(function(article) {
22 return(
6. <div key={article.id}>
24 <hr/>
25 <h4><Link to={`/articles/${article.id}`}>{article.title}</Link></h4>
26 <small>id: {article.id}</small>
27 </div>
28 )
29 })
30 );
31 }}
32 </Query>
33 </div>
34 )
35 }
36
37 export default ArticleList;
Now let's test it out. Make sure the API server is running. Open another window and go to the project's root directory. Then run:
nodemon
Your API server should now be running on localhost:4000. If you are using a local version of MongoDB also make sure that is running in a separate window (mongod
). You may need to restart your Client server. Go back to the window running the React app. Stop the server with CTRL+C
. Then run:
yarn start
And now your front end React app should be running on localhost:3000 and should be displaying the articles saved to your database.
On the articles page in your browser if you click on an article title it should take you to the /articles/:id route and the ArticleInfo component. It will be empty because we haven't entered any code yet. So let's do that now:
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import { Query, Mutation } from 'react-apollo';
4 import { GET_ARTICLE, DELETE_ARTICLE, GET_ARTICLES } from '../../graphql/articleQueries';
5
6 function ArticleInfo(props) {
7 return (
1. <Query query={GET_ARTICLE} variables={{ id: props.match.params._id }}>
9 {function({ loading, error, data }) {
10 if (loading) return "Loading...";
11 if (error) return `Error! ${error.message}`;
12 const { article } = data;
13 return (
14 <div>
15 <h2>{article.title}</h2>
16 <small>id: {article.id}</small>
17 <p>{article.content}</p>
18 <p className="btn-group">
19 <Link to={`/articles/${article.id}/edit`} className="btn btn-info">Edit</Link>
2. <Mutation mutation={DELETE_ARTICLE} >
3. {function(deleteArticle, { data }) {
22 return(
23 <button className="btn btn-danger"
4. onClick={() => {
5. deleteArticle({
6. variables: { id: article.id },
7. refetchQueries: [{query: GET_ARTICLES}]
28 });
8. props.history.push("/articles");
30 }}
31 >
32 Delete
33 </button>
34 )
35 }}
36 </Mutation>
37 <Link to="/articles" className="btn btn-secondary">Close</Link>
38 </p>
39 <hr/>
40 </div>
41 );
42 }}
43 </Query>
44 )
45 }
46
47 export default ArticleInfo
Now let's dig into mutations. On the article page we will have a button to delete the article. When the user presses the button we need to send the deleteArticle mutation to the GraphQL API with the article.id property. We imported the Mutation component from react-apollo.
In the graphql/articleQueries file we created a GraphQL mutation string calling deleteArticle, wrapped it with a gql function, assigned it to the DELETE_ARTICLE constant, and imported it to our current ArticleInfo component.
update(cache, { data: { createArticle } }) { const { articles } = cache.readQuery({ query: GET_ARTICLES }); cache.writeQuery({ query: GET_ARTICLES, data: { articles: articles.filter(a => (a.id !== article.id)) }, }); }
Try viewing and deleting an article in the browser. We'll cover creating an article next. You can add back articles using the GraphQL playground at localhost:4000 (if using apollo-server) or the Graphiql tool in the browser at localhost:4000/graphql (if using express-graphql).
mutation { createArticle(articleInput: { title: "Learn React", content: "Lorem Ipsum."}) { id, title, content } }
On the articles page in your browser there is a button that will take you to the /articles/new route and the ArticleAdd component. This displays a form to create a new article.
1 import React from 'react';
2 import { Link } from 'react-router-dom';
3 import { Mutation } from 'react-apollo';
1. import { CREATE_ARTICLE, GET_ARTICLES } from '../../graphql/articleQueries';
5
6 function ArticleAdd(props) {
7 let title, content;
8
9 return (
2. <Mutation mutation={CREATE_ARTICLE}>
3. {function(createArticle, { data }) {
4. return(
13 <div>
14 <h4>Add Article</h4><hr/>
15 <form
5. onSubmit={function(event) {
6. event.preventDefault();
7. createArticle({
8. variables: { title: title.value, content: content.value },
9. refetchQueries: [{query: GET_ARTICLES}]
20 });
10. props.history.push("/articles");
22 }}
23 >
24 <div className="form-group">
25 <label>Title:</label>
26 <input type="text" className="form-control"
11. ref={function(node) { return title = node; }}
28 />
29 </div>
30 <div className="form-group">
31 <label>Content:</label>
32 <textarea rows="5" className="form-control"
33 ref={function(node) { return content = node; }}
34 />
35 </div>
36 <p className="btn-group">
37 <button type="submit" className="btn btn-primary">Submit</button>
38 <Link to="/articles" className="btn btn-secondary">Cancel</Link>
39 </p>
40 </form>
41 </div>
42 )}
43 }
44 </Mutation>
45 );
46 };
47
48 export default ArticleAdd;
update(cache, { data: { createArticle } }) { const { articles } = cache.readQuery({ query: GET_ARTICLES }); cache.writeQuery({ query: GET_ARTICLES, data: { articles: articles.concat([createArticle]) }, }); }
Test it out by adding an article in the browser.
We are now on the last component in our React front end, the edit page to Update an existing article in the database. On the article info page in your browser there is an Edit button that will take you to the /articles/:id/edit route and the ArticleEdit component. This is another form like ArticleAdd, but we need to populate it with the article's existing values.
1 import React from 'react';
2 import { Query, Mutation } from 'react-apollo';
3 import { GET_ARTICLE, UPDATE_ARTICLE } from '../../graphql/articleQueries';
4
5 function ArticleEdit(props) {
6 function handleCancel(id) {
7 props.history.push(`/articles/${id}`);
8 }
9
10 let title, content;
11 return (
12 <Query query={GET_ARTICLE} variables={{ id: props.match.params._id }}>
1a. {function({ loading, error, data }) {
14 if (loading) return "Loading...";
15 if (error) return `Error! ${error.message}`;
16 const { article } = data;
17 return (
18 <div>
19 <h1>Edit {article.title}</h1>
20 <Mutation mutation={UPDATE_ARTICLE}>
1b. {function(updateArticle, { loading, error }) {
22 return(
23 <div>
24 <form
25 onSubmit={function(event) {
26 event.preventDefault();
2. updateArticle({
28 variables: {
29 id: article.id,
30 title: title.value,
31 content: content.value
32 }
33 });
34 props.history.push(`/articles/${article.id}`);
35 }}
36 >
37 <div className="form-group">
38 <label>Title</label>
39 <input type="text" className="form-control"
3. defaultValue={article.title}
41 ref={function(node) { return title = node; }} />
42 </div>
43 <div className="form-group">
44 <label>Content</label>
45 <textarea rows="5" className="form-control"
46 defaultValue={article.content}
47 ref={function(node) { return content = node; }} />
48 </div>
49 <div className="btn-group">
50 <button type="submit" className="btn btn-primary">Update</button>
51 <button type="button" className="btn btn-secondary"
52 onClick={function() { handleCancel(article.id) }}>Cancel</button>
53 </div>
54 </form>
55 {loading && <p>Loading...</p>}
56 {error && <p>Error : {error.message}</p>}
57 </div>
58 )
59 }}
60 </Mutation>
61 </div>
62 );
63 }}
64 </Query>
65 )
66 }
67
68 export default ArticleEdit;
Test it out by editing an article in the browser.
This completes our last component. There are two more brief topics, both optional, then we are done.
Apollo has created a plug-in for Chrome Developer Tools that is pretty useful. You can download it free at chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm
Right now we need separate terminal windows open to run our back end API server (port 4000) and our front end React app (port 3000). Not to mention our MongoDB server. For convenience we can run our API and React apps with one command using the Concurrently package. Make sure you are in the project's root directory (not in the client folder). Using the --save-dev flag installs it as a development environment dependency.
npm install concurrently --save-dev
// package.json
...
"scripts": {
"start": "node server.js",
"dev": "concurrently \"nodemon server.js\" \"cd client && npm run start\""
},
...
"devDependencies": {
"concurrently": "^4.1.1"
},
This will run the nodemon command on the API application starting the server on port 4000. Then it will cd to the client directory and run the start command which will run the React app on port 3000. To execute this script, make sure you stop both servers first, then from the project's root directory run:
npm run dev
This should start both servers and open the React app in your browser.
You now have a fully functioning GraphQL app using Node.js, MongoDB, and React with all four CRUD capabilities. That's a pretty advanced stack. Not bad.