Ditching Babel

It might be time to ditch Babel (even for React projects)

Have you used the jsComplete playground before? It executes React code and understands JSX and modern JavaScript features. It does that without using Babel. I’ll tell you how in this article.

The "Why"

If you work on a React project, chances are you have to deal with Babel. It is needed for 2 main tasks:

  1. To compile JSX into React.createElement API calls

  2. To compile bleeding-edge JavaScript into something that most browsers can understand

These are 2 good reasons to have something like Babel in your stack. However, what you might not know is that Babel is NOT the only tool to do the 2 tasks above. You can also use the TypeScript compiler for that!

If you’re not a fan of TypeScript, I understand. I took my sweet time to get on the TypeScript wagon and I am not suggesting you make a jump today. You can use the TypeScript compiler to only do the 2 tasks above. You don’t have to "really" use TypeScript and start adding types. TypeScript will compile your code even if it thinks you have type-related problems!

Why give up Babel for TypeScript?

Babel is just a complier that translates one form of code into another. That’s why we call it a "transpiler" because nerds like to merge words together.

TypeScript, on the other hand, is a language. Not only is the TypeScript compiler able to translate one form of code into another but it also comes with other BIG advantages. Advantages you don’t know you’ll appreciate until you see them in action. By moving to TypeScript, you’re taking the first step into making your code base a much better place. With one flag switch, you can start improving your code using the powerful hints that TypeScript can give you.

You still need a module bundler

While TypeScript can bundle you code into one file, that support is still primitive and has many issues. You should still use a bundler like Webpack, Rollup, Parcel, etc.

All of these bundlers can be made to work with TypeScript. I use Webpack for the jsComplete playground (and it works great) but for the simple example here I’ll use Parcel because it has built-in support for TypeScript and it is really the simplest bundler to use (in development).

I’ll also use yarn because it’s a much better package manager but you don’t have to use it to get things rolling for this example. The npm command will work fine too but I encourage you to also give up npm for yarn. You will not miss npm. I promise.

The "How"

While there are lots of code-generation tools you can use to get a React project working with TypeScript, to show you how simple things will get, let’s make a project from scratch.

mkdir -p my-app/src/app/components
cd my-app
yarn init -y
yarn add react react-dom typescript parcel-bundler

Believe it or not, these 4 dependencies are all you need to get a basic project going.

The -p in mkdir makes the command create each directory in the path it receives. The mkdir command above will create 4 nested directories.

A sample React app

Let’s test things out with the a sample React app. I’ll use the standard "hooks" example:

import React, { useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);
  return (
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me

Put this in app.tsx somewhere. For example, under the src/app/components directory.

The directory structure does not matter. You can use whatever you like. However, the use of the .tsx extension is a great example of convention over configuration. By naming any file that has JSX with a .tsx extensions you are signaling that this file needs a special step before it can be used.

You actually don’t HAVE to use the .tsx extension with Parcel. It works fine with .js/.jsx but using the .ts/.tsx extension has more benefits.

Use conventions over configurations when you can. It’s not just about using the conventions of a fraework (or library). You should make your own code leverage this very powerful concepts.

The App in app.tsx is just a component. We need to render it:

import React from 'react';
import ReactDOM from 'react-dom';

import App from './components/app';

ReactDOM.render(<App />, document.getElementById('mountNode'));

This file also contains JSX. Put it in src/app/index.tsx. This is the JS "entry" point for the app and that’s why I like to use the name index for it.

Youi also need an HTML entry point to host the JS entry one and give it its mountNode element:

<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <title>Hello React</title>
    <div id="mountNode"></div>
    <script src="./index.tsx"></script>

You can put that in src/app/index.html.

Configuring TypeScript

Not all "conventions" will work out for you all the time; you cannot escape configurations. What works for you in development has to be configured differently in production and what works for you in a browser has to be configured differently server-side. No one is suggesting that configurations are bad but the less configurations you have to deal with the better. If something has a smart default that can be used, then use it.

In the root of the project, run the yarn tsc --init command. This will create a tsconfig.json file. Scan through this file to see all the options that you can configure for TypeScript.

TypeScript has a lot of configurations! However, because we’re not planning to use it for its powerful types (yet) and want to use it only for the 2 tasks that we started with, we only need to configure a few properties. Replace everything in that tsconfig.json file with:

  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "jsx": "react",
    "outDir": "./build",
    "skipLibCheck": true,
    "allowJs": true
  • The target property is what will make TypeScript compile the bleeding-edge JavaScript into the ES5 that all browsers support today. You can use newer targets if you need to target just modern browsers and make your bundle size smaller.

  • The module, moduleResolution, and esModuleInterop properties are to let TypeScript know that we’re executing things with Node and we’d like to use the import/export syntax.

  • The jsx property is what will make TypeScript compile JSX into React.createElement calls.

  • The outDir is only needed in production and only if you’re doing anything "server-side". All the front-end code will be prepared by your bundler (and you’ll need to configure where it goes there).

  • The skipLibCheck property will make TypeScript skip the checking of any TypeScript declaration files in your dependencies and that speeds up the compile time.

  • The allowJs property is only needed if you have .js files in your project (which do not have JSX) and you don’t want to change them. However, I encourage you to rename all .js files into .ts. In that case, you don’t need this allowJs property.

Why use .ts? Because I have a (not-so-)hidden agenda for you. I want you to start exploring what TypeScript can do for you, but one step at a time. This is just your first step and it does not require you to learn anything about TypeScript. You just use it to run things. When you’re ready to start exploring you can test things gradually. Stay tuned for more articles about exploring TypeScript (for both Node and React).

Renaming files in an existing project can be done with one or two commands:

  • To rename all .jsx files into .tsx you can use this command:

    find . | grep "\.jsx$" | xargs rename -f 's/\.jsx$/.tsx/'
  • To rename all .js files with JSX content into .tsx, you can use a command like:

    git grep -I --files-with-matches -E "</.*>|/>" | grep "\.js$" | xargs rename -f 's/\.js$/.tsx/'

    Unfortunately, there might be false negatives and positives for this one. You’ll need to do a manual check after.

  • To mass rename any remaining .js files to .ts, you can use this command:

    find . | grep "\.js$" | xargs rename -f 's/\.js$/.ts/'

You’re welcome.

Make sure these rename changes go into a git commit by themselves. Also, try not to put your name on that commit because you’ll be git-blamed for everything, forever. Learned that the hard way.

That’s all you need for TypeScript to compile your code. You can test things out by running the yarn tsc command in the root of the project and inspect what TypeScript puts under the "build" directory. JSX will be properly converted and the import/export syntax is converted to work with Node. If you use modern JavaScript in your code, it’ll get converted too.

The "level" of conversion is configurable. You can tell TypeScript what syntax not to convert. However, TypeScript will not "polyfill" missing features. That is a job for another tool (for example, ts-polyfill).

Configuring Parcel

Well, for development purposes, Parcel just works out of the box. No configurations are needed. You can give it an entry HTML file and it will do all the bundling and run a development server for you. How cool is that!?

yarn parcel src/app/index.html

Then head over to http://localhost:1234/ and you should see the counter React app in there. Done.

Parcel has a lot of surprising things that will just work for you out of the box. For example, hot-reloading is on by default. Go ahead and try to change something in your component and see how the browser will auto-reload the state of the app. You can even start using code-splitting without needing to add any configurations to your development environment.

Note that Parcel will create and use a different "out" directory. The default is ./dist. This is different from the outDir used by TypeScript which we configured to be ./build. TypeScript’s outDir directory will have an exact copy of your code structure, file by file, not bundled. This is needed if, for example, you want to run things server-side. This ./outDir directory does not need to be "public". Parcel’s ./dist is your React project bundled for browsers. It’s the one that needs to be made public. You would not execute the content of this directory server-side at all.

For production you will have to configure Parcel to do things differently. I’ll leave that bit for another article.