Zac Fukuda
028

GraphQL + MongoDB: React

The source code is available on Github.

Following the previous three articles, I am going to show you how to build a simple GraphQL API server that communicates MongoDB, with the implementation of front-end React application.

Simple React-GraphQL application
Screenshot of our React-GraphQL application

You can find the proceeding tutorials at…

  1. GraphQL + MongoDB: Basic – Query
  2. GraphQL + MongoDB: Basic – Mutation
  3. GraphQL + MongoDB: Express

Prerequisites

  • Node.js & NPM, and Yarn installed.
  • MongoDB installed.
  • Previous experience in Node.js application development with MongoDB.

As of this writing, I tested the sample code shown below with Node.js v8.11.1 and MongoDB v3.4.4. (Now MongoDB v4.0 is out.) I cannot guarantee that the code I give you here works with the later version of Node.js and MongoDB. So if you encounter any bugs due to the version difference, please find a solution on your own.

Before Getting Started

From now on, please follow the tutorial opening Terminal.

Keep Running MongoDB Demon

While you are following this tutorial, please make sure that your local MongoDB is up running. (You need to open two Terminal windows: One for Mongo Demon and another for running Node.js. I assume that you are using iTerm.)

$ mongod

Let’s create-react-app

To initiate our Node.js project I use create-react-app. So in Terminal please guide you to the directory that you want to make the application and run create-react-app.

// If you don’t have create-react-app installed,
$ npm install -g create-react-app

$ cd PATH/TO/YOUR/DIR
$ create-react-app graphql-mongo-react
$ cd graphql-mongo-react

Of course you can change the name of directory to anything you want other than "graphql-mongo-react".

After creating the project, please remove unnecessary files.

You may want to yarn start before removing files shown below in order to check if your React application works.

rm src/App.css src/App.js src/App.test.js src/index.css src/logo.svg src/registerServiceWorker.js

Server files

Since I have explained how to build GraphQL-MongoDB API server in the previous tutorial, I will not show how to do that. Please make a new directory called server in your project root.

$ mkdir server

If you have followed the previous tutorial, please copy and paste files into that directory from your previous working directory except package.json, yarn.lock, and node_modules—we install packages at the new project root. For those who didn’t follow the previous tutorial, please download source files from Github, and copy and paste them.

After copying the server-related files, let’s install necessary NPM packages to run GraphQL API server.

$ yarn add express express-graphql graphql mongoose

You can run GraphQL server by node server/sever.js, or it is helpful to add a script to package.json to do the same thing with yarn server:

./package.json
{
  ...
  "scripts": {
    ...
    "server": "node server/server.js"
  }
}

The server will be run at http://localhost:4000, and you can visit GraphiQL at http://localhost:4000/graphql.

In this tutorial I use database called graphql. Since it is a common name, if you already have a database in the same name, please replace dbName in config.js with anything you like.

Getting Started

Our React components are designed as follows:

React components design
React components design

It is possible to make component files one by one when we need them, but let us create every JS files we need this moment.

$ touch src/BookLibrary.js src/BookList.js src/Book.js src/AddForm.js

I am going to show you how to build the application in order of Query, Mutation(add), and Mutation(remove).

Query

First thing you need is Query function in order your application to retrieve books data from database and display it. Let’s modify index.js to render BookLibrary component instead of App, which is being used by default by create-react-app. Removing all lines that we don’t need, the final index.js would look like this:

./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import BookLibrary from './BookLibrary';

ReactDOM.render(<BookLibrary />, document.getElementById('root'));

Basic React App

Before diving into implementing function that sends query request, let’s first create a simple React app that displays book data statically defined inside its component. Please add following code to BookLibrary.js.

./src/BookLibrary.js
import React, {Component} from 'react';
import BookList from './BookList';

class BookLibrary extends Component {

  constructor(props) {
    super(props);
    this.state = { books: []};
    this.componentDidMount = this.componentDidMount.bind(this);
  }

  componentDidMount() {
    let books = [
      {_id: '1', title: 'David and Goliath', author: 'Malcolm Gladwell'},
      {_id: '2', title: 'Steve Jobs', author: 'Walter Isaacson'}
    ];
    this.setState({books: books});
  }

  render() {
    return(
      <div>
        <h3>Book Library</h3>
        <BookList books={this.state.books} />
      </div>
    );
  }
}

export default BookLibrary;

BookLibrary renders BookList component, passing books data as props to it. You know what to do next. Les’s create BookList.

./src/BookList.js
import React, {Component} from 'react';
import Book from './Book';

class BookList extends Component {

  render() {
    return (
      <ul>
        {this.props.books.map(book => (
          <Book key={book._id} title={book.title} author={book.author} />
        ))}
      </ul>
    );
  }
}
 
export default BookList;

What BookList does is simple. Given book data from its parent component, it renders Book component for each data. Now it is time to create Book.

./src/Book.js
import React, {Component} from 'react';

class Book extends Component {

  render() {
    return(
      <li key={this.props._id}>{this.props.title}, <em>{this.props.author}</em></li>
    );
  }
}

export default Book;

At this point, you may want to test your application whether it works or not. Please run yarn start to start your React application. The browser will open automatically guiding you to http://localhost:3000.

Implementing Fetch API

If there is no problem running the program you just wrote, let us implement request capability. We are going to use Fetch API—or fetch()—to send requests to server. Please update componentDidMount() of BookLibrary.js as follows:

  componentDidMount() {

    const option = {
      method: 'POST',
      headers: { 'Content-Type': 'application/graphql' },
      body: '{ books {_id, title, author} }'
    }
    fetch('/graphql', option).then( res => {
      return res.json();
    }).then( json => {
      this.setState({books: json.data.books});
    });
  }

In the code above, you see that the query for GraphQL is passed to option.body. You might expect that, as I did, res is JSON format string that contains data value, which has books value of array of book data. Unfortunately, fetch() does not work that way. If you want to know how to deal with Response of fetch(), please check out github/fetch.

Using axios is another option to send HTTP request from browser.

Updating package.json

Basically we have to run two servers: one is Webpack server that serves front-end React application; another is GraphQL-Express backend server. You can run two servers simultaneously with two Terminal windows opened. But it would be much better if we can run those servers with one command. That’s how npm-run-all comes in. This module enables us to run multiple npm-scripts in parallel or sequential. In our case, we want to run scripts start and server in parallel.

You can also use concurrently to run multiple NPM scripts.

For the host, since our two servers are run at different ports, we have to proxy our API request to port 4000. If your project is initialized with create-react-app, this can be done easily by adding proxy field to package.json.

Les’s install npm-run-all to our NPM project and make a modification to package.json.

$ yarn add --dev npm-run-all
./package.json
{
  ...
  "scripts": {
    ...
    "parallel": "run-p server start"
  },
  ...
  "proxy": "http://localhost:4000"
}

Query Test

Having made these files, you are ready to send Query request. Please run yarn parallel in Terminal.

$ yarn parallel

This command starts your Webpack server and your GraphQL server. If you have followed the previous tutorials, you may see a list of some books you manipulated before. If you haven’t, you see no book data because there is no data yet in your database.

Technically, if you haven’t followed the previous tutorials, the database itself doesn’t even exist yet. You can proceed without seeing book data in browser, but if you want to generate seed data now, please follow next steps:

  1. Make server/seed.js.
  2. Copy content from here.
  3. Run node server/seed.js.

In my React directory from Github, I eliminated seed.js because I can generate data from form that we create in the next step.

Mutation - Add

If your Query request works well, it is time to try Mutation. In this application, we implement two manipulations: one for adding book data and another for removing it. Les’s begin with adding part.

Updating BookLibrary

Your BookLibrary needs to import form component and render it. Therefore, please update your BookLibrary as follows:

./src/BookLibrary.js
...
import AddForm from './AddForm';

class BookLibrary extends Component {

  constructor(props) {
    ...
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  componentDidMount() {
    ...
  }

  async handleSubmit(data) {}

  render() {
    return(
      <div>
        <h3>Book Library</h3>
        <BookList books={this.state.books} />
        <AddForm onSubmit={this.handleSubmit} />
      </div>
    );
  }
}

export default BookLibrary;

I am going to show you how the inside of handleSubmit() would look like later. First, let us create AddForm component.

Creating AddForm

Unlike traditional <form>, with React form itself doesn’t have to send HTTP request. Instead, what it need to do is to construct input values and pass them to parent component triggering certain event. You see in the code above, onSubmit of AddForm it triggers BookLibrary.handleSubmit(). In other words, onSubmit works like an elevator that conveys data from child to parent component.

Please add following code to AddForm.js.

./src/AddForm.js
import React, {Component} from 'react';

class AddForm extends Component {

  constructor(props) {
    super(props);
    this.state = { title: '', author: ''};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(e) {
    const target = e.target;
    const name = target.name;
    const value = target.value;

    this.setState({
      [name]: value
    });
  }

  handleSubmit(e) {
    e.preventDefault();

    if ( !this.state.title || !this.state.author) return;

    const data = {
      title: this.state.title,
      author: this.state.author
    }

    this.setState({title: '', author:''});
    this.props.onSubmit(data);
  }

  render() {
    return(
      <form onSubmit={this.handleSubmit}>
        <label>Title</label>: <input type="text" name="title" value={this.state.title} onChange={this.handleChange} /><br />
        <label>Author</label>: <input type="text" name="author" value={this.state.author} onChange={this.handleChange} /><br />
        <input type="submit" value="Add" />
      </form>
    );
  }
}

export default AddForm;

For how to work with form element with React, their official website has comprehensive documentation.

Completing handleSubmit

Back to BookLibrary, let’s complete handleSubmit() that we eft empty earlier. Given the data with two values from AddForm, handleSubmit(), that sends Mutation request, would look like this:

./src/BookLibrary.js
  async handleSubmit(data) {

    const query = `mutation {
      addBook (title: "${data.title}", author: "${data.author}") {
        _id
        title
        author
      }
    }`;

    const option = {
      method: 'POST',
      headers: { 'Content-Type': 'application/graphql' },
      body: query
    }

    const res = (await fetch('/graphql', option));
    const json = (await res.json());
    const newBook = json.data.addBook;
    const books = this.state.books;
    const newBooks = books.concat(newBook);
    
    this.setState({books: newBooks});
  }

It is possible to fetch() like we do in componentDidMount. This time, however, I decided to adapt async/await syntax, just in order to make it simpler. Although the end code looks easier to understand for some developers, following standard promise syntax might improve application performance because all process is asynchronous. Well, that is what a promise promised to us. Anyway, I just wanted to show you there are two approaches to achieve the basically same jobs.

Mutation - Add Test

Let’s test our application.

$ yarn parallel

This time you see the form element under the list of books. Furthermore, even if you didn’t follow the previous tutorials before, you can generate book data from browser fulfilling form. So far so good.

Mutation - Remove

Since it is same Mutation type, removing and adding book data works similar way. The only difference is that in removing part data will be passed up involving two levels of components—Book, BookList, to BookLibrary.

Updating BookLibrary again

To be able to handle removing event, let’s add a new method to BookLibrary and bind a new event to BookList that it renders.

./src/BookLibrary.js
...

class BookLibrary extends Component {

  constructor(props) {
    ...
    this.handleRemove = this.handleRemove.bind(this);
  }

  componentDidMount() {
    ...
  }

  async handleSubmit(data) {
    ....
  }

  async handleRemove(title) {}

  render() {
    return(
      <div>
        <h3>Book Library</h3>
        <BookList books={this.state.books} onRemove={this.handleRemove} />
        <AddForm onSubmit={this.handleSubmit} />
      </div>
    );
  }
}

export default BookLibrary;

Updating BookList and Book

We are adding remove button to Book component, on the click of which the component passes its title value to BookList, and then it relays that data to BookLibrary. The end code of each file would look as follows:

./src/BookList.js
import React, {Component} from 'react';
import Book from './Book';

class BookList extends Component {

  constructor(props) {
    super(props);
    this.handleRemove = this.handleRemove.bind(this);
  }

  handleRemove(title) {
    this.props.onRemove(title);
  }

  render() {
    return (
      <ul>
        {this.props.books.map(book => (
          <Book key={book._id} title={book.title} author={book.author} onRemove={this.handleRemove} />
        ))}
      </ul>
    );
  }
}

export default BookList;
./src/Book.js
import React, {Component} from 'react';

class Book extends Component {
  
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    e.preventDefault();
    const title = e.target.value;
    this.props.onRemove(title);
  }

  render() {
    return(
      <li key={this.props._id}>{this.props.title}, <em>{this.props.author}</em> <button type="button" value={this.props.title} onClick={this.handleClick}>x</button></li>
    );
  }
}

export default Book;

In this application, we use title value to identify which data to remove from database. This is due to the fact that in the previous versions of GraphQL-MongoDB application it is hard to know _id of each book. Thus I used title as an identifier instead. In practical application, you must use _id as an identifier to determine which data to manipulate.

It was possible to modify our GraphQL API server from previous version. But since I didn’t want to make any change to it for this tutorial, I decided to go with title.

Completing handleRemove

Having implemented the program that relays data from Book to BookLibrary, let us go back to BookLibrary and complete handleRemove().

  async handleRemove(title) {

    const query = `mutation {
      removeBook (title: "${title}") {
        _id
      }
    }`;

    const option = {
      method: 'POST',
      headers: { 'Content-Type': 'application/graphql' },
      body: query
    };

    const res = (await fetch('/graphql', option));
    const json = (await res.json());
    const removeBook = json.data.removeBook;
    const books = this.state.books;

    // Remove book that matches _id from array
    const index = books.findIndex( book => book._id === removeBook._id );
    if ( index !== -1 ) {
      books.splice(index, 1);
      this.setState({books: books});
    }
  }

Test

Now all our coding job is done. It is time to run the complete application. Before final testing, if you want to check all source codes you wrote, please compare them with source files on Github. (Note that a few comments are added to source files.)

Once again, to start the servers please run:

$ yarn parallel

If you see the similar window to the screenshot I showed you at the beginning of this tutorial, please add data fulfilling form or click “x” button to remove data.

Conclusion

So far, in this and proceeding articles, we learned how to build GraphQL API server that communicates with MongoDB, with front-end React application to interact with it.

Although you can build similar application with tools such as Prisma, I believe that understanding basics—and being able to build it—is beneficial to every developers. The downside is…it takes time. In our application, we have to define scheme of GraphQL on the backend and corresponding query requests on front-end. Who knows what is happening on both sides? Therefore, we need some eco system that generates all scheme and query in single-handed way. The good news is Prisma for MongoDB is upcoming. Even with Prisma, it still requires much of time and effort, it will reduce them in a significant way.

Please exploit the knowledge you learned in this tutorial, and build much better application on your own.