Set up a React project without Create React App

Setting up a React project from scratch is no small feat if you have not done it before. Not knowing what the packages and commands do can be quite scary. That is why projects like Create React App exist, to simplify the bootstrapping process.

However, we are not going to use Create React App today and instead do a deep dive into setting up a simple React project from the ground up.

Prerequisites for this tutorial include having node and npm installed. If you do not have these installed, there are many articles online that show you how to do just that. I recommend having nvm to easily switch between node versions. I am using node 16.19.1, but node 18 is also fine.

You can also find the project repository on GitHub, which has the completed code from this article.

Project Initialization

To start, let’s open up your terminal and create a new project folder in your preferred directory by running mkdir my-react-app. Navigate into the project folder by running cd my-react-app.

Once in the project folder, we want to initialize a new npm project. You can do this by running npm init and answering the prompts. The answers don’t really matter at this point, they are just to set up a boilerplate project. Once finished, you will see a new package.json file in your project. This file contains metadata about your project and the dependencies you will use.

Installing NPM Packages

Let’s install React and ReactDOM. We are going to need these to create our React app. Run npm install react react-dom to install the packages.

Next, we will install some development dependencies to help us build this app. Install them by running the following:

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader css-loader mini-css-extract-plugin webpack webpack-cli webpack-dev-server

Here is what our package.json looks like so far:

{
  "name": "my-react-app",
  "version": "1.0.0",
  "description": "A sample React project setup without create-react-app",
  "main": "index.js",
  "scripts": {
    "start": "webpack serve --mode development --open",
    "build": "webpack --mode production"
  },
  "author": "Paul Kim",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.21.0",
    "@babel/preset-env": "^7.20.2",
    "@babel/preset-react": "^7.18.6",
    "babel-loader": "^9.1.2",
    "css-loader": "^6.7.3",
    "mini-css-extract-plugin": "^2.7.5",
    "webpack": "^5.76.1",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  }
}

Observant readers will note that the package.json above has an extra scripts section. Go ahead and manually add it to your package.json.

By now you’ve installed a number of packages that you may or may not know what they do. Much of it might not mean anything yet, but stick with it and I promise you it will make more sense.

Babel Configuration

Babel is a compiler for modern JavaScript that transpiles new language features to older versions of JavaScript that are supported by most browsers. This means developers can use the latest features of JavaScript without having to worry about browser support and environment. When we installed @babel/core, this is what we got.

Babel is also highly configurable and extendable with plugins and presets. We are using two presets in this project. @babel/preset-env is a collection of plugins that allow Babel to understand and transpile modern JavaScript syntax to the appropriate browser targets. @babel/preset-react is a preset that allows Babel to transform JSX syntax into plain JavaScript.

To begin working with Babel and its presets, we need to tell Babel what to use. Create a new file called babel.config.json in your project directory and add the following:

{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ]
}

This tells Babel to run the modern JavaScript we write through these presets to generate older JavaScript code.

Webpack Configuration

Webpack can be a bit intimidating when first setting it up. Just the sheer number of configurations can leave any developer feeling lost. We are going to try and dispel some of that mystery today. Create a new file called webpack.config.js in your project directory and add the following code. If you don’t know what all this does, don’t worry. We will walk through each section below.

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles.css',
    }),
  ],
  devServer: {
    port: 8080,
  },
};

Let’s step through each section of the configuration and look at what they do.

  entry: './src/index.js',

This is the entry point of our application. This is where Webpack starts reading our code and building our dependency graph.

  output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},

This is the output configuration for our bundled assets. This specifies where the bundle should be saved and what name it should have. In our case, we are saving the bundle as bundle.js and saving it in the dist directory.

  module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},

This is the module configuration for our Webpack build. This tells Webpack how to handle different types of files in our application. It allows you to use loaders to transform and process different file types, like JavaScript, CSS, and images.

In our case, we have two rules. The first rule uses babel-loader to transpile our JavaScript code using Babel. It applies the loader to all .js files, except those in the node_modules directory, and uses the babel.config.json file to configure Babel. The second rule uses mini-css-exctract-plugin and css-loader to load and extract CSS code to create separate .css files.

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'styles.css',
    }),
  ],

The plugins configuration is where you can add plugins to extend the functionality of Webpack. Plugins can perform a variety of tasks, like generating an HTML file for your application, compressing your output files, and more.

In this case, we only have one plugin. We are using mini-css-exctract-plugin to extract CSS code to a file called styles.css and output it in our dist directory.

  devServer: {
    port: 8080,
  },

Lastly, we have the development server configuration. This tells Webpack how to server our application during development. In our case, we are telling Webpack to listen on port 8080 and serve index.html, which we will create next.

This covers the basics of our Webpack configuration, but there are a lot more options. You can refer to the Webpack documentation for more information on configuration options.

Source Files

Let’s create a new directory called src in our project root by running mkdir src. This is where all our pre-compiled source files will live. In the public directory, create a new file called index.html and add the following HTML markup. This markup is extremely simple and would not be used in a production environment, but we will be using it to display your React application.

<html>
  <head>
    <meta charset="utf-8">
    <title>My React App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="./bundle.js"></script>
    <link rel="stylesheet" href="./styles.css">
  </body>
</html>

Next, create a new file called index.js in your src directory and write the following code. This will render your React application in the DOM.

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

We will then create the <App /> component that we defined in the step above. Create an App.js file in your src directory and add the following. In our case, this will be a very simple Hello World application, but this can easily be any application we want it to be.

import React from 'react';

import './App.css';

const App = () => {
  return (
    <div>
      <h1>Hello, World!</h1>
    </div>
  );
};

export default App;

Let’s also write the CSS file we imported in the step above. In your src directory, create an App.css file and write the following code:

h1 {
  color: red;
  font-size: 32px;
  text-decoration: underline;
}

That’s it! We now have all our source files and are ready to start development.

Development and Build

Open your terminal and run npm run start to start the development server. After a second or two, your browser should open up to http://localhost:8080 and show you your Hello World application. You can open your project folder in your favorite code editor and start editing your React application. Try changing the color in the App.css file. Or add a paragraph to your App.js file. Any changes you make will be automatically updated in your browser. So awesome!

Below is a demo of what you should see when you have everything running correctly. You can make changes in the files and see how they update. Go ahead, give it a try!

Once you are done with your changes, run npm run build to compile the production-ready version of your application. This command will create a dist folder in your project directory that contains all your files. Deploy the dist folder to your preferred hosting platform to make your React application live.

At this point, we’ve set up a React project without using Create React App. I hope you found this helpful in understanding the underlaying mechanisms that go into building a React application. Thanks so much for reading!