Code snippets, tutorials and software engineering.

Introduction

Looking into learning React? Want to utilize reusable components to make your developing life simpler? In this article, we will learn how to set up a simple React app to begin your React.js career. We will be using Express for the backend, along with a MongoDB database.

This article will not go too in-depth into React, Express, or MongoDB, but will primarily get you started in using React. Using Express and MongoDB as well are a way to allow you to build something rather complete, and introduce you to how all these technologies can work together. Maybe you are here because your preferred stack is MERN (MongoDB, Express, React, Node) or maybe you are here just because you have an assignment using this stack and need guidance. Whatever the reason is, you'll come away with a basic understanding of how all these technologies can produce a simple, yet working application, and hopefully you can soon develop one on your own. Let's get started.


Why React and Express?

First of all, why are we using React.js for this project? For one, it is a popular library. It is also easy to use, understand, and implement. That is partly the reason you are here, is it not? Aside from being in-demand, React allows us to create reusable UI components that will be useful in the project we will later discuss. With React, we will be able to manipulate data without reloading the webpage, a feature that is also necessary for creating an app that provides a great user experience.

Express is a web framework for Node.js. Express makes handling the backend of our application simple, since it is very easy to use and easy to learn. Routing requests have never been simpler than with Express. This simplicity is what makes Express a highly sought after tool to learn backend development. Combining React with Express and MongoDB creates what is known as a MERN Stack. All these tools can build dynamic websites and web apps. The best part is that they all support JavaScript, so you can build an entire full-stack app using one language.

Now that we have given a brief background on some of our libraries and frameworks, we can now discuss our example project.


The Example Project

In this example, we will be tackling a project that displays a list of books using the Google Books API. Users will be able to search for books that Google Books offers. From there we will allow them to be saved to the user's own "Saved" library. How this works is that when we view books that similarly match our search parameter, we can decide to save them to our database. Once we have saved a book, that book and its information is now in our database. When we view our "Saved" library, we retrieve all books from our database, our saved books. This is a fairly simple application. It will introduce you to React components and pages, as well as show you how to utilize some of the basic React hooks. We will talk more about these later.

First, let us create our application. Let's begin with our main directory, I have named it googlebooks. If we cd into it, we'll have an empty directory. Let's first create our main file, server.js. This can be done by typing touch server.js in our root directory. In order to get started with our Node project, we will first run the command npm init -y. A package.json file will be created.

We will also need to install a few dependencies:

  • axios - A promise based http client for the browser
  • express - framework for server-side logic
  • mongoose - MongoDB ODM

We can install all these by running the following in your terminal:

npm i axios express mongoose

Once these are installed, we can now get started with creating and establishing our connection with our database using Mongoose and Express.


Connect to MongoDB Backend using Express

First, we will create a routes directory for our URL handlers, a models directory for our MongoDB schema models, and a db directory for our MongoDB connection. Your file structure should look similar to this:

googlebooks
|-- package.json
|-- package-lock.json
|-- server.js
|-- routes/
|-- models/
|-- db/

Let's tackle our server.js file. Here we are going to establish our connection to our database and our Express server.

Your server.js file will look like this:

Let's talk about what we did here. Lines 1-4, we loaded Express in the variable express and other necessary variables. We also created our express app in the variable app. Lines 6-11 is our middleware. In lines 6-8, we served our static assets to our server. Later on, we will create a directory named client for our React frontend. For now, know that we are serving static files from our build output to our client. Line 10 will parse requests with URL-encoded payloads. Finally, line 11 will parse requests with JSON payloads. In line 13, we loaded in our routes for our API endpoints. We will get back to this later. In lines 15-19, we send our app a route path for every route we are at. Note the '/*'. Here any route that follows '/' will be sent the files of our static build from our React app. In lines 21-26, we connect to our database, and if successful, we tell our app to listen on port 3001 if we are working on our local machine, or process.env.PORT for whichever port is given to us by our hosting platform.

Now we will show how to connect to MongoDB. In our db directory, we will create a file named index.js. Here our file will look as follows:

module.exports = require('mongoose').connect(process.env.MONGODB_URI || 'mongodb://localhost/<db_name>', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true,
  useFindAndModify: false
})

This file will establish our connection to our MongoDB. Because of some deprecation warnings provided by Mongoose, we set some fields as true or false. For more information on these deprecation warnings, look here.

If our app is already in production, then we'll connect to MongoDB using process.env.MONGODB_URI. In our hosting platform, we will provide MONGODB_URI as an environment variable. This will be a connection string that can be found in your MongoDB Atlas account. More instructions on how to retrieve this string can be found here. If we were working in our local environment, we can connect to our database through the MongoDB Compass application. The connection string would be mongodb://localhost/<db_name>. In this example, <db_name> can be replaced with googlebooks since this will be our collection name.

Once we have set up our database connection, we can simply run the command:

node server.js

If our terminal prints Server Connected!, then all is well, and we can move on to testing our models.


Creating Mongoose Models

Earlier, we created a models directory. In this directory, we will create two files, index.js and Book.js. In index.js, we will have the following:

module.exports = { Book: require('./Book.js') }

This file will allow us to export any model out of our models directory. In our case, we only have the one, but if we were to make this project more complex by adding more models, we'll include the rest here as well. Our Book.js file will be as shown below:

const { model, Schema } = require('mongoose')

const Book = new Schema({
  title: {
    type: String,
    required: true
  },
  authors: [ String ],
  description: {
    type: String
  },
  image: {
    type: String
  }
}, { timestamps: true })

module.exports = model('Book', Book)

Our Book model is simple. We only care about a few data attributes given back from our Google Books API request. We only need the title of the book, an array of authors, a book description, and a book image. Mongoose will automatically give each object model an id, which we will use later. As we can see, the title of each book is of type String, the authors is an array containing elements of type String, the description is of type String, and the image is a URL link of type String. Mongoose allows us to set the types for the model's properties. To learn more about Mongoose Object Models, look here.


Creating CRUD Routes for Backend

Now we will move onto our CRUD routes. In this case we will only be using a GET and POST route for our database, and a GET route for accessing the Google Books API. Earlier, we created a routes directory. This directory will have three files, index.js, bookRoutes.js, and googleBookRoutes.js. As used in our models directory, the index.js file will export a Router object built from both our bookRoutes and our googleBookRoutes. This Router object is capable of performing routing functions, such as HTTP methods of GET, PUT, POST, and so on. The index.js file will look as follows:

const router = require('express').Router()

router.use('/api', require('./bookRoutes.js'))
router.use('/api', require('./googleBookRoutes.js'))

module.exports = router

Here, we export our two routing files. Whenever an HTTP request is followed by /api, we will then direct it to either our bookRoutes or our googleBookRoutes. We will go more in-depth into how the request will differentiate between the two.

Our googleBookRoutes.js file will send a GET request to the Google Books API. Below you will see how we do so:

When the HTTP request of our app looks like this, /api/gbs/:search, then we will make a request to Google Books. Here, /:search is our search parameter. This will be the keyword that is given to us from our frontend search box. We will use axios, a promise based HTTP client, to handle our request. From our request, we will be given a response, which we will use the destructuring assignment on in order to get the data object. This object is a large array of all books that contain the search keyword. Lines 7-19 show that we first map all books in data to only render the authors, the description, the title, and the id. We also check to see if any id is equivalent to any id of a book already in our database. If that's the case, we do not want to render a book that is already saved. Otherwise, we render all books displaying only the information we've requested.

Now, onto our bookRoutes.js file. This file will handle our HTTP requests to our database. Because we are only handling GET and POST routes, this file will be fairly simple.

const router = require('express' ).Router()
const { Book } = require('../models')

router.get('/books', (req, res) => {
  Book.find()
    .then(books => res.json(books))
    .catch(err => console.log(err))
})

router.post('/books', (req, res) => {
  Book.create(req.body)
    .then(book => res.json(book))
    .catch(err => console.log(err))
})

module.exports = router

As you can see, any time our HTTP request has the path /api/books, we will get all books currently saved in our database. Using the Mongoose query .find() on our model, will "find" all objects in our Book collection. For POST routes to our database, we use the query .create() with the request body as our parameter. This essentially "creates" a new object in our database with the given request body as the object's properties. For more information on Mongoose queries, you can look here.


Testing Routes

We will briefly test our routes to see how, and if, they work. First we will begin with searching for books using the Google Books API. Here, I create a GET request in Postman. The request is as follows: http://localhost:3000/api/gbs/harry%20potter. This sends a request to the API for any book that has the words "harry potter". The output of Postman is shown below, a list of books for the search "harry potter".

So searching the Google Books API is a success. Now we will manually take the data of one of these books and send a POST request to our database. With the POST request http://localhost:3000/api/books and a request body of the information of one book, we successfully created a new object in our database. The output of Postman is shown below:

To learn more about Postman, click here. Now that we have created and tested our routes for both accessing the Google Books API and our database, we can move on to creating our React app.


Connect to React.js Frontend

We will now create our React frontend. We can do this by simply creating our application in our terminal with create-react-app. We should be in our root directory googlebooks. If not, go ahead and cd into it by running the command cd googlebooks. Then we will run the following:

npx create-react-app client

Here, create-react-app creates a frontend for us. We can use it with any backend. All frontend work will be done in this client directory, while all the backend will be in our root directory named googlebooks.

So now we cd into client to get started. First, let's install a few dependencies. We will again need axios, but we will also need react-router-dom. We will run the command npm i axios react-router-dom. This will install these two dependencies into our application and will now be visible in our package.json. We will also need to configure the proxy in order to let our React app communicate with our backend. In the package.json file in our client, we need to add a "proxy" line. The beginning of our package.json will look like this:

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:3001",
  "dependencies": { ... }
}

The port in the proxy needs to match with whatever port your Express server is running on. In our case, PORT 3001. We will also need to remove some of the base code that create-react-app gives us. Inside client, ensure that your file structure looks like this:

client
|-- package.json
|-- package-lock.json
|-- public/
|-- src/
    |-- App.js
    |-- index.js
    |-- logo.svg
    |-- reportWebVitals.js
|-- .gitignore
|-- node_modules/

Go ahead and remove any files inside src that are not necessary. We will also be making some changes in index.js before we get started. Make sure this file looks like the following:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import reportWebVitals from './reportWebVitals'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

We will also be editing App.js, but for now we will move forward to building out components and pages. Of course, while developing yourself, you will find yourself doing a lot of testing and working through things step by step, usually dealing with many files at a time. In this example, we will go through each aspect of the React app separately.


Building out Utility functions

We will begin with a utils directory for any utility functions and code that will be required throughout our React app. Create this directory within src. You will run the following command:

cd src
mkdir utils
cd utils

In utils we will create an API directory. This will be for easy access to all our API functions. We will also create a BookContext directory for our useContext hook. We will create two files in each directory. Let's do both at the same time. While in utils, run the following:

mkdir API BookContext
touch API/index.js API/API.js BookContext/index.js BookContext/BookContext.js

Now we can begin with API. Go on ahead and cd API, and open index.js. This file is rather simple, containing the one line: export { default } from './API.js'. This will export our API.js file to any place we need it. In API.js, we will have the following:

import axios from 'axios'

const API = {
  getBooks: search => axios.get(`/api/gbs/${search}`),
  getSavedBooks: () => axios.get('/api/books'),
  saveBook: book => axios.post('/api/books', book)
}

export default API

In this file, we create some API functions that can be easily accessed anywhere within our React app. Calling API.getBooks(search) with the parameter "search" will make an axios.get() request. This is done for organization and readability.

Now we cd out of API and into BookContext. Inside BookContext, we have two files, index.js and BookContext.js. Again, index.js will export our BookContext.js file. It's contents will include one line: export { default } from './BookContext.js. BookContext.js will be using the Context from React. This will allow us to share data and values through our React components without having to pass props through every level. The file will look like the following:

import { createContext } from 'react'

const BookContext = createContext({
  search: '',
  books: [],
  handleInputChange: () => { },
  handleSearchAPI: () => { }, 
  handleSaveBook: () => { },
  saved: []
})

export default BookContext

We created variables and functions to share between our components. Here, search is our "search" string to be passed to our Google Books API, books is our result array from our Google Books API response, and saved is an array of our saved books that are in our database. We also created functions for the various events that we will have to respond to. First, handleInputChange will allow us to update the search box upon each keystroke, handleSearchAPI will perform our form submission for searching a book using the Google Books API, and lastly, handleSaveBook will handle "clicking" the "Save" button on a book in order to send its data to the database. We can now move onto building out our components.


Building React Components

We need to first navigate back to our src directory. From our BookContext directory, we will simply run cd ../.., and now we are in src. Let's create a components directory with the command mkdir components, and go ahead and cd components. Inside components, we will create four React components and each will have their own directory. Each directory will have two files, an index.js file and another file with its corresponding name. We can see how this is done with the following commands:

mkdir Form Navbar SavedBook SearchedBook
touch Form/index.js Form/Form.js
touch Navbar/index.js Navbar/Navabar.js
touch SearchedBook/index.js SearchedBook/SearchedBook.js
touch SavedBook/index.js SavedBook/SavedBook.js

To be concise and brief, each index.js file will have a similar format. They will each have one line, as shown below:

export { default } from './Form.js' // for Form/index.js

export { default } from './Navbar.js' // for Navbar/index.js

export { default } from './SearchedBook.js' // for SearchedBook/index.js

export { default } from './SavedBook.js' // for SavedBook/index.js

Now, onto our components. Our first component, in Form.js, is our form. This is where we will create a form with a search box and a button to perform the action of accessing the Google Books API. Here, we use our BookContext, in which we utilize the useContext hook. Below is how the file is written:

import { useContext } from 'react'

import BookContext from '../../utils/BookContext'

const Form = () => {
  const {
    search,
    handleInputChange,
    handleSearchAPI
  } = useContext(BookContext)

  return (
    <form onSubmit={handleSearchAPI}>
      <label>
        Search:
        <input 
          type="text"
          name="search"
          value={search} 
          onChange={handleInputChange}
        />
      </label>
      <button onClick={handleSearchAPI}>Search</button>
    </form>
  )
}

export default Form

As we can see, we use handleSearchAPI when we submit our form and handleInputChange on our text input box. The value of that input is set to whatever we have in search. We will discuss more on what these functions do later on.

Our Navbar.js file holds our Navbar component. This one is rather simple:

import React from 'react'

import { Link } from 'react-router-dom'

const Navbar = () => {
  return (
    <>
      <h4>Google Books</h4>
      <Link to="/">Search</Link>
      <Link to="/saved">Saved</Link>
    </>
  )
}

export default Navbar

We use react-router-dom's <Link> tag as a way to provide navigation. If the path of our application is followed by /, then we are directed to the Search page. If the path is followed by /saved, then we are directed to the Saved page.

Next is our SearchedBook.js file for our book components displayed when we perform a search on the Google Books API. The file looks as follows:

import React from 'react'

const SearchedBook = props => {
  return (
    <>
      {
        props.book.image.length > 0 ? (
          <img
            src={props.book.image}
            alt={props.book.title}
          />
        ) : (
          <h5>No Image Available</h5>
        )
      }
      <div>
        <h5>{props.book.title}</h5>
        <h6>Written By: {props.book.authors.join(', ')}</h6>
        <p>{props.book.description}</p>
      </div>
      <div>
        <button onClick={() => props.handleSaveBook(props.book.bookID)}>Save</button>
      </div>
    </>
  )
}

export default SearchedBook

First, we use React Fragments, <></> to contain our code, since React requires components to be wrapped inside an enclosing element if there are multiple children. We initially check to see if the Book object passed by props has an empty image URL string. If so, when we render the information, we will display to the user that there is "No Image Available". Otherwise, we display the image provided. We also render the title, the list of authors, and the book description. Following all the book's information, we render a button for the user to click. The button uses the handleSaveBook context provided by its parent element passed down through props. For more information on how props work, look here.

Lastly, we will show how our SavedBook component is done. In SavedBook.js, we have a file identical to SearchedBook.js, but is missing the "Save" button:

import React from 'react'

const SavedBook = props => {
  return (
    <>
      {
        props.book.image.length > 0 ? (
          <img
            src={props.book.image}
            alt={props.book.title}
          />
        ) : (
          <h5>No Image Available</h5>
        )
      }
      <div>
        <h5>{props.book.title}</h5>
        <h6>Written By: {props.book.authors.join(', ')}</h6>
        <p>Type: {props.book.description}</p>
      </div>
    </>
  )
}

export default SavedBook

We again check for an image URL string, and proceed with the same render output as SearchedBook, minus the "Save" button.

Now that all our components are built out, let's move on to our pages.


React Pages

We will have two pages, a Search page and a Saved page. Each will have two files, and index.js file and another with the corresponding name. We first head back into src. Once in our src directory, we can create our new directories for our pages. We will do so with the following commands:

mkdir pages
cd pages
mkdir Search Saved
touch Search/index.js Search/Search.js
touch Saved/index.js Saved/Saved.js

Just like the components, the index.js files in both will be similar, one line as shown below:

export { default } from './Search.js' // for Search/index.js

export { default } from './Saved.js' // for Saved/index.js

In our Search.js file, we handle our functions from BookContext.js. Take a look here:

import React, { useState } from 'react'

import API from '../../utils/API'
import BookContext from '../../utils/BookContext'
import Form from '../../components/Form'
import SearchedBook from '../../components/SearchedBook'

const Search = () => {
  
  const [searchState, setSearchState] = useState({
    search: '',
    books: []
  })

  searchState.handleInputChange = event => {
    setSearchState({ ...searchState, [event.target.name]: event.target.value })
  }

  searchState.handleSearchAPI = event => {
    event.preventDefault()

    API.getBooks(searchState.search)
      .then(({ data }) => {
        setSearchState({ ...searchState, books: data, search: ''})
      })
      .catch(err => console.error(err))
  }

  searchState.handleSaveBook = bookID => {
    const saveBook = searchState.books.filter(x => x.bookID === bookID)[0]

    API.saveBook(saveBook)
      .then(() => {
        const books = searchState.books.filter(x => x.bookID !== bookID)
        setSearchState({ ...searchState, books })
      })
  }

  return (
    <>
      <h6>Search Books</h6>
      <BookContext.Provider value={searchState}>
        <Form />
        {
          searchState.books.length > 0 ? (
            searchState.books.map(book => (
              <SearchedBook
                key={book.bookID}
                book={book}
                handleSaveBook={searchState.handleSaveBook}
              />
            ))
          ) : null
        }
      </BookContext.Provider>
    </>
  )
}

export default Search

In this file, we use a useState hook. Here, we declare a "state variable" called searchState. When we perform any action, like filling in our text input on our form, searching the Google Books API, or adding to our database, we need to update the state of our searchState. We do this by setting setSearchState. Our searchState has two properties, search and books. When we type in our input box, we update search with whatever value it currently holds. This happens every time we hit a key. So, for example, if typing "harry potter", our search property will update from the first letter "h" to the last, until the value of search is "harry potter". This is done through the searchState.handleInputChange() function which then updates search in our BookContext. A similar action is performed on the books property in our searchState. This is an array, initially empty, but will be updated once we submit our form. Our form submission calls searchState.handleSearchAPI(). This function will call our API.getBooks(search), where our search parameter is the value in search. The books array will then be updated with a list of all books related to our search word, and will then render to the page. Our search property will then reset for another use. Lastly, searchState.handleSaveBook will be executed when the user clicks the "Save" button on our SearchedBook component. When this happens, API.saveBook(book) is called on the book that was clicked. That book is sent to the database, and the books array is now updated with the saved book removed. Now onto the Saved page.

This page is located in the Saved.js file. In this file, we again use the useState hook. We also use the useEffect hook. In this page, we use the useEffect hook with an empty array as its dependency to run the callback function only once after initial rendering. The file is shown below:

import { useState, useEffect } from 'react'

import API from '../../utils/API'
import SavedBook from '../../components/SavedBook'

const Saved = () => {

  const [savedState, setSavedState] = useState({
    saved: []
  })

  useEffect(() => {
    API.getSavedBooks()
      .then(({ data }) => {
        setSavedState({ ...savedState, saved: data })
      })
  }, [])

  return (
    <>
      <h6>Saved Books</h6>
      {
        savedState.saved.length > 0 ? (
          savedState.saved.map(book => (
            <SavedBook
              key={book.bookID}
              book={book}
            />
          ))
        ) : null
      }
    </>
  )
}

export default Saved

We use useEffect here to update our savedState's saved array. To do so, we call API.getSavedBooks(), which gets all books in our database, and thus renders them to the screen after saved array is updated. If saved is empty, then we do not render any books and thus have an empty Saved page.

In both the Search and Saved pages, we pass on props to our SearchedBook and SavedBook components. This allows us to pass "properties" down to our components for future use. In this case, we pass down a key prop to control component instances, and a book prop, which is the data for each book in either the Search page or the Saved page.

Like said previously, we will edit the App.js file in src now that all components and pages are completed. In this file we will basically start from scratch. Here's how it should look:

import React from 'react'

import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import Search from './pages/Search'
import Saved from './pages/Saved'
import Navbar from './components/Navbar'

const App = () => {
  return (
    <Router>
      <div>
        <Navbar />
        <Switch>
          <Route exact path="/" component={Search} />
          <Route path="/saved" component={Saved} />
        </Switch>
      </div>
    </Router>
  )
}

export default App

Here we utilize many components of react-router-dom. We use <Route> to render our pages for that specific path. We use <Switch> to render our routes for our pages exclusively. If our path is /, then we will only be directed to the Search page. You can see we also render our Navbar here.

Now that our app is complete, minus styling, we will now show how it works.


Testing Application

When in our root directory, googlebooks, run the following command npm run start. Now navigate to http://localhost:3000/ in your browser.

Here is our initial page:

Now we have searched "harry potter":

Now we have saved the first book, it is no longer displayed:

On our Saved page, the book we saved is now present:


All done!

Awesome, now we have successfully developed a basic full-stack MERN application. You have gotten an introduction to React, and how it works with Express and MongoDB. Next, I will work on how to deploy such an application. Good luck on developing your own similar apps, look back on this guide for any advice, as well as any links that were provided throughout.

You’ve successfully subscribed to Into Code
Welcome back! You’ve successfully signed in.
Great! You’ve successfully signed up.
Your link has expired
Success! Check your email for magic link to sign-in.