This was originally posted here
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
Part Three : Building the Movie App using component library
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
Initialise 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.
Initialising 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 initialise 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
Initialise 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
Phew!!! That was a long one.
Now, the final step is to configure Continuous Integration and Deployment to make sure every-time we make changes, it gets deployed seamlessly. You can read about it in the last instalment of this series.
Top comments (1)
Hi @debojitroy ,
Thank you very much for this amazing series.
This was just what I was looking for and I couldn't find any other tutorial that explained all of this so clearly.
Only a little suggest:
Maybe it's easy to use yarn to add multiple dependencies at once:
yarn workspace @awesome-movie-app/components add styled-components react-bootstrap bootstrap -D
I also had to add this option to the tsconfig file to avoid errors in files outside the src folder:
"skipLibCheck": true
And well, there is one thing that I do not quite undestand. Why do you compile using typescript and then again using babel ? Then in webapp I think you usually import the babel compiled files except the ones in the theme folder ( compiled by typescript ). Wouldn't compiling using typescript be enough ?
Thank you again for this helpful tutorial and access to the github repo.