React projects can be scaffolded in many different ways, predominantly by using Facebook's Create React App or Vercel's Next.js.
However, Create React App is notorious for being unscalable. Enterprise-level apps that uses CRA will inevitably forced to invoke the much dreaded react-scripts eject
command that unfolds the nicely abstracted curtains and exposes all the underlyings of the setup so that further customizations can be made. Nonetheless, it is an absolutely daunting task to take on.
Next.js is great but I want a vanilla React project to work with, so I decided to create one on my own. Here is the tutorial on how I wired things up and get it up and running.
First, we will need a package.json
file and it can be generated by using NPM. The -y
flag defaults all the prompt and generate the package.json
that can be worked on immediately.
npm init -y
Create a src
folder, and a public
folder on the root. Install React and React-DOM accordingly.
pnpm i react react-dom
Add the typing for React and TypeScript as dev dependencies.
pnpm i -D @types/react @types/react-dom typescript
Create a tsconfig.json
with the following configurations.
{
"compilerOptions": {
"lib": ["es6", "dom"],
"allowJs": false,
"jsx": "react",
"esModuleInterop": true,
"noImplicitAny": true,
"strictNullChecks": true
},
"exclude": ["node_modules", "dist", "public"]
}
Create an entry point for the application in src
folder named App.tsx
and populate with the following contents. What this does is just inject the <h1>Hello world</h1>
into a blank html page.
import React from 'react'
import ReactDOM from 'react-dom/client'
const App = () => <h1>Hello world</h1>
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(<App />)
Next, create the blank index.html
file inside the public
folder with the following contents.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React ESBuild</title>
</head>
<body>
<div id="root"></div>
<script src="/dist/bundle.js"></script>
</body>
</html>
ESBuild
pnpm i -D esbuild
ESBuild can be executed via the ESBuild executable or Node.js with a ESBuild config file. The example command for the executable looks like this:
esbuild src/App.tsx --bundle --minify --sourcemap --outfile=public/dist/bundle.js
Running the command will gives us the output as below.
public\dist\bundle.js 139.1kb
public\dist\bundle.js.map 340.2kb
Done in 35ms
However, we will be using the config file version because it is cleaner and offers more customization in my opinion.
Create the esbuild.config.js
with the following contents. What this does is just compile the code starting from the entry point specified and output the bundle files into public/dist/bundle.js
. It is also set in a watch mode so that rebuild is triggered each time when there is changes to the files that it is listening to.
The onRebuild
function is called each time a rebuild is triggered, and it is the great place to logs the status of the rebuild.
require('esbuild')
.build({
entryPoints: ['src/App.tsx'],
bundle: true,
minify: false,
format: 'cjs',
sourcemap: false,
outfile: 'public/dist/bundle.js',
watch: {
onRebuild(error, result) {
var now = new Date()
if (error) {
console.log(
'🙈\x1b[2m %s: \x1b[0m\x1b[37m\x1b[41m%s\x1b[0m %s',
now.toTimeString(),
'FAILURE',
error.message
)
} else {
console.log(
'🐻❄️\x1b[2m %s: \x1b[0m\x1b[30m\x1b[42m%s\x1b[0m %s',
now.toTimeString(),
'COMPLETE',
'Build successful'
)
}
},
},
})
.then(() => console.log('watching...'))
.catch(() => process.exit(1))
After that, add the run script to package.json
for both the watch version and the build version of the ESBuild.
{
"scripts": {
"build": "esbuild src/App.tsx --bundle --minify --sourcemap --outfile=public/dist/bundle.js",
"watch": "node esbuild.config.js"
}
}
Lite Server
Next, we will need to have a server that can host the files and listen to changes. We will use lite server for this.
pnpm i -D lite-server
Create a bs-config.js
file to store configurations related to lite server. The only settings we need is setting the base directory of the server to the public
folder where the index.html
resides.
module.exports = {
server: {
baseDir: './public',
},
}
Add a run script in package.json
to start the server.
"start": "lite-server"
Running the script with pnpm start
and you will see the output from lite server as such.
[Browsersync] Access URLs:
---------------------------------------
Local: http://localhost:3000
External: http://192.168.68.109:3000
---------------------------------------
UI: http://localhost:3001
UI External: http://localhost:3001
---------------------------------------
[Browsersync] Serving files from: ./
[Browsersync] Watching files...
ESLint
To generate a ESLint config file, run the following command.
npm init @eslint/config
A command dialog will prompt for the preferred options to use with ESLint and select accordingly (not necessary follow my choice).
npx: installed 41 in 5.263s
√ How would you like to use ESLint? · style
√ What type of modules does your project use? · esm
√ Which framework does your project use? · react
√ Does your project use TypeScript? · Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · YAML
Checking peerDependencies of eslint-config-standard-with-typescript@latest
Local ESLint installation not found.
The config that you've selected requires the following dependencies:
eslint-plugin-react@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.0.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No
Successfully created .eslintrc.yml file in C:\Users\my-project
Select No
when prompted to install the dependencies as I presume that it will use NPM to install and generate a package.lock.json
file which in my case I am using Pnpm. Install the dependencies separately.
pnpm i -D eslint-plugin-react@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.0.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 @typescript-eslint/parser
One more step to silence the warning that says unspecified React version is to append this at the bottom of .eslintrc.yml
.
settings:
react:
version: 'detect'
After that add the following run script into package.json
.
"lint": "eslint src/**/*.tsx"
The run script will lint all the files that end with tsx
within the src
folder. Running ESLint is as easy as
pnpm lint
And the example output for ESLint that contains error are as shown.
C:\Users\MyProject\src\App.tsx
1:19 error Strings must use singlequote @typescript-eslint/quotes
1:26 error Extra semicolon @typescript-eslint/semi
2:22 error Strings must use singlequote @typescript-eslint/quotes
2:40 error Extra semicolon @typescript-eslint/semi
4:24 error Strings must use singlequote @typescript-eslint/quotes
4:45 error Extra semicolon @typescript-eslint/semi
6:13 error Missing return type on function @typescript-eslint/explicit-function-return-type
11:2 error Extra semicolon @typescript-eslint/semi
13:34 error Forbidden non-null assertion @typescript-eslint/no-non-null-assertion
13:58 error Strings must use singlequote @typescript-eslint/quotes
13:67 error Extra semicolon @typescript-eslint/semi
14:21 error Extra semicolon @typescript-eslint/semi
✖ 21 problems (21 errors, 0 warnings)
18 errors and 0 warnings potentially fixable with the `--fix` option.
Starting the App
To start the app, we will need two separate terminals. The first terminal listens to the changes in the React files and compile them when new changes are made.
pnpm watch
The second is running the lite server to serve the app locally.
pnpm start
Up Next
The next step is to setup a testing framework as well as using Tailwind for styling. Setting up the testing framework with @testing-framework/react
and Jest on my own is absolutely painful as the toolchains are convoluted and confusing for me that had always taken granted for the out-of-the-box and low-setup tests settings while using Create React App or Next.js.
I had spent countless hours debugging the issues faced, installing and uninstalling packages and meddling round with tonnes of config files and fortunately able to make everything works. I think that setting up tests in this project deserves a separate article on its own, so stay tuned.