React Project - Idea to Production - Part Three - Building the Movie App using component library

Tue May 26 20209 min read

This is the third post in the series. You can find the first post here

Where are we

Ok so till now we have

  • Brainstormed on our brilliant idea to build a Movie App.
  • We have decided what features are needed as part of the MVP.
  • Our design team has given us the wireframes.
  • We have setup our project as a Monorepo.
  • We have setup linting rules, code formatter and commit hooks.
  • We have setup our our component library
  • We added support for Typescript in our component library
  • We have setup Storybook
  • We added our components to the component library
  • We have added unit tests for our components
  • We can see our components showcased in Storybook

What are we going to do now

Ok so the next step is to build the movie app using the component library. We will be using TMDB for fetching our movie details. We will maintain our application state using Redux. We will use Webpack to bundle our application. At the end of this post we should have converted our wireframes to an actual working website.

TL;DR

This is a 4 part post

Source Code is available here

Component Library Demo is available here

Movie App Demo is available here

Extracting common functionality in core

It is always advisable to extract common services to keep it DRY. As we extracted common components in our previous post, we will extract common functionality in core.

What resides in core

The definition of common functionality is very broad and there are more than one way to skin the chicken 🐔 For our project we will extract our api calls in core

Setting up core

Move to the packages folder

cd packages

Create a new folder for our core

mkdir core
cd core

Initialize the yarn project

yarn init

Following the steps for naming, as we did in our previous post, our package.json looks like

{
  "name": "@awesome-movie-app/core",
  "version": "1.0.0",
  "description": "Core Services for Awesome Movie App",
  "main": "index.js",
  "repository": "git@github.com:debojitroy/movie-app.git",
  "author": "Debojit Roy <debojity2k@gmail.com>",
  "license": "MIT",
  "private": true
}

Building core

Adding axios

We will be making a lot of XHR calls to fetch data. We can choose to use browser’s native AJAX functionality or the shiny new fetch api. With so many browsers and different implementation of fetch it is safer not to use fetch. If we choose to include fetch we will have to add the required polyfills.

So it is much better to go ahead with axios which will make sure our network calls work correctly irrespective of the user’s browser.

Initializing config variables

As core is a common library, we don’t want to hardcode, nor dictate how the environment variables are set. We would like to delegate it to the calling project to decide.

So we will create a bootstrap file which will be used to initialize the config.

let config: { url: string; apiKey: string } = { url: "", apiKey: "" }

export const setConfig = (incomingConfig: { url: string; apiKey: string }) => {
  config = incomingConfig
}

export const getConfig = () => config

Adding search service

One of the first things as per our requirement was to add a search service. We are going to use the Search Endpoint

After mapping the response, the functionality looks something like this

import axios from "axios"
import isNil from "lodash/isNil"
import { getConfig } from "./bootstrap"

export interface SearchResult {
  popularity: number
  vote_count: number
  video: boolean
  poster_path: string
  id: number
  adult: boolean
  backdrop_path: string
  original_language: string
  original_title: string
  genre_ids: number[]
  title: string
  vote_average: number
  overview: string
  release_date: string
}

export interface SearchResponse {
  page: number
  total_results: number
  total_pages: number
  results: SearchResult[]
}

export const searchMovie = async (
  queryString?: string
): Promise<SearchResponse> => {
  const config = getConfig()

  if (isNil(queryString) || queryString.trim() === "") {
    return new Promise(resolve => {
      resolve({
        page: 1,
        total_pages: 1,
        total_results: 0,
        results: [],
      })
    })
  }

  const encodedQuery = encodeURI(queryString)

  const result = await axios.get(
    `${config.url}/3/search/movie?api_key=${config.apiKey}&query=${encodedQuery}`
  )

  return result.data
}

We will continue mapping rest of the functionality, the complete code is available here

Setting up Web Application

Now with the required services mapped out, we will focus on building the actual web application.

Splitting out code in this way helps to re-use functionality without copy pasting things over and over again.

Major parts of our webapp will be

  • Public Files
  • Webpack config
  • Common parts
  • Feature specific segregation

WebApp Project setup

Move to the packages folder

cd packages

Create a new folder for our webapp

mkdir webapp
cd webapp

Initialize the yarn project

yarn init

Following the steps for naming, as we did in our previous post, our package.json looks like

{
  "name": "@awesome-movie-app/webapp",
  "version": "1.0.0",
  "description": "Web Application for Awesome Movie App",
  "main": "index.js",
  "repository": "git@github.com:debojitroy/movie-app.git",
  "author": "Debojit Roy <debojity2k@gmail.com>",
  "license": "MIT",
  "private": true
}

Setting up public assets

So for the React project to mount, we need a DOM element, where React can take over and inject the elements. For this purpose we need a index.html file which will be served by the server before React takes over.

We will keep this index.html in our public folder, but feel free to choose any other name.

You can find the file here Feel free to name the folder and files as you want, but make sure to update the same in the webpack config in the next step.

Setting up Webpack

We will use webpack to package our application. You can choose any other packager for your project and make changes accordingly.

Prepare the config folder

mkdir config

Setting up shared configuration

For our local development we will be using webpack dev server and production build and minification for production build. But some of the steps will be common for both, we will extract those in our common config.

So our common config looks something like this

// webpack.common.js
const path = require("path")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const HtmlWebPackPlugin = require("html-webpack-plugin")

const isEnvDevelopment = process.env.NODE_ENV === "development"
const isEnvProduction = process.env.NODE_ENV === "production"

module.exports = {
  entry: { main: "./src/entry/index.tsx" },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx"],
  },
  node: {
    fs: "empty",
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|mjs|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              hmr: isEnvDevelopment,
            },
          },
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              ident: "postcss",
              plugins: () => [
                require("postcss-flexbugs-fixes"),
                require("postcss-preset-env")({
                  autoprefixer: {
                    flexbox: "no-2009",
                  },
                  stage: 3,
                }),
                require("postcss-normalize"),
              ],
              sourceMap: isEnvProduction,
            },
          },
        ],
        // Don't consider CSS imports dead code even if the
        // containing package claims to have no side effects.
        // Remove this when webpack adds a warning or an error for this.
        // See https://github.com/webpack/webpack/issues/6571
        sideEffects: true,
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: ["file-loader"],
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: ["file-loader"],
      },
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebPackPlugin({
      title: "Awesome Movie App",
      template: "./public/index.html",
      filename: "./index.html",
      favicon: "./public/favicon.ico",
    }),
  ],
}

Most of the things are self explanatory. If you are new to webpack, I would suggest checking out their awesome documentation

Setting up the dev config

With common config setup, we would like to setup our dev config. We want to use webpack dev server and hmr with routing fallback.

Our dev config looks like

//webpack.dev.js
const path = require("path")
const merge = require("webpack-merge")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const common = require("./webpack.common.js")

module.exports = merge(common, {
  mode: "development",
  devtool: "inline-source-map",
  output: {
    path: path.join(__dirname, "../../dist/dist-dev"),
    filename: "[name].[contenthash].js",
    publicPath: "/",
  },
  devServer: {
    contentBase: "./dist-dev",
    historyApiFallback: true,
    allowedHosts: [".debojitroy.com"],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
  ],
})

Building the common parts

Common parts are feature agnostic pieces which have the cross cutting functionality.

Common - Components

These are the common components which will be used across the features.

Common - Config

Configurations for applications which are defined here.

Common - Redux

Redux specific files will be stored here.

Common - Routes

Routing specific files will be stored here.

Common - Utils

Common utilities will be added here.

Building Features

Features is where the actual features of the application will be kept. Think of each feature as a standalone piece of the application. Each feature in itself should be able to stand apart. For demonstration purpose we will look into SiteHeader feature.

SiteHeader - Components

This part will contain all our React components as the name suggests. Based on the functionality required we will break down our feature in components.

SiteHeader - Redux

This is where all Redux related files will be added.

I am skipping over these sections fast as they are standard React / Redux stuff which are better explained in many other places.

Getting the webapp running

Adding .env

We need to declare the config variables for running our application. In our production step we will be doing it differently. For local development let’s add .env file and add it to .gitignore so that it doesn’t get checked in.

Go to webapp

cd packages/webapp

Create a .env file

vim .env

Add the config values

API_URL=https://api.themoviedb.org
API_KEY=<Replace with actual key>

Preparing launch script

Now once we have .env setup, last thing we need to do is add the start script.

Open package.json inside webapp and add this under scripts

"start": "cross-env development=true webpack-dev-server --config config/webpack.dev.js --open --port 8000"

Running Webapp locally

Once we are done setting up webapp, lets try to run it locally.

First, build your components

cd packages/components
yarn build-js:prod

Second, build your core

cd packages/core
yarn build-js:prod

Finally start your webapp

cd packages/webapp
yarn start

If everything went well, you should see something like this

Movie App Local Screencap

Phew!!! That was a long one.

Now, the final step is to configure Continous Integration and Deployment to make sure everytime we make changes, it gets deployed seamlessly. You can read about it in the last installment of this series.

javascript

reactjs

redux

redux-saga

typescript

webpack

axios

tmdb

Built using Gatsby