Implementing React with Parcel
By Jedai Saboteur
January 7, 2020 11:37 PM
Sometimes I want a lightweight React implementation I can worth with quickly. I don’t necessarily want or need all the bells and whistles create-react-app puts onto my (virtual) table. I also don’t want to muddle around with webpack or Babeljs configuration. I want to be up and going fast. That’s why for this tutorial, I’ll be using Parcel.
Parcel is a bundler that utilizes Babel to transform Javascript syntax and bundle the code for distribution. Many features such as importing Javascript modules and styles are available with ‘zero configuration’. We're going to be doing more configuration than zero though, because we'll be doing things that aren't support out-of-the-box.
For this tutorial, you should have a basic understand of BASH and Node. I'm using Node version 12.13.1. You should also have a basic understanding of React, though it's not required if you follow the instructions closely. I would still suggest reading React's tutorial if you haven't played with it yet.
We're making a simple React app that reveals a single letter of a word we choose each time a user pushes a button. To start out, we need to get our project directory prepared.
mkdir parcel-react && cd parcel-react && mkdir src && touch src/index.js src/index.html src/style.css src/App.js src/WordReveal.js
The line above creates a new directory for our project, changes our current directory to that location, creates an src
directory, then adds several files we'll be working with to it.
Now, initialize your project with the package manager of your choice. I'm using yarn. I just used default values.
yarn init -y
Next we need to get Parcel. Their official documentation suggests that you have it installed globally, but you could also install it as a developer dependency and we'll be doing that in this case.
yarn add -D parcel-bundler@1.12.4
I'm targeting a specific version here, in case any of our dependencies have breaking changes from the time I write this tutorial to the time you read it. You can target specific dependency versions by appending @<version>
to the end of the package name as shown above.
You may notice parcel has a lot of dependencies, and avoiding that bloat on a repo is part of the reason you may want to install it globally. Global installs are especially useful if you utilize parcel across many projects. If you're using CI or don't have enough access to the machine to install Parcel globally, you'll have to go this route.
Now, we'll need React and ReactDOM.
yarn add react@16.12.0 react-dom@16.12.0
ReactDOM is like the glue between the 'actual' DOM and what what React says the DOM should be, according to what components you create and how you modify them over time. It's also responsible for rendering your React components. The React package gives us access to the many React features we're familiar with or have been told about. This is a hypercondensed explanation and it's definitely worth your time to read the React documentation and try the tutorial if this is all new to you.
Next, we're going to add some simple markup to index.html
.
<html>
<body>
<div id="app"></div>
<script src="./index.js"></script>
</body>
</html>
Yes, this markup leaves a lot to be desired, but it gets the job done and this tutorial is free. The important takeaway here is that you don't need much markup for React to work. The most important lines are those within the body. The first is the element inside which we are going to render our React app. The second is a reference to that app's Javascript.
index.html
is our Entry File for Parcel. Everything we're processing (Javascript code, CSS, HTML, etc) is considered an 'asset'. Parcel constructs a tree of assets (starting with our Entry File), and then makes a bundle tree from it, which is used to create our final bundle. See the docs for a much more thorough explanation.
Now, we need to set up React in index.js.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("app"));
This little script imports React and our App, then renders the App component in the div we provide.
Next we make the app itself. We'll start with a 'hello world' of sorts, just to make sure everything is working correctly.
import React from "react";
class App extends React.Component {
render() {
return <div>This should work!</div>;
}
}
export default App;
We arguably don't need a class for this tiny example, but we're going to add some more things to it later. In the meantime, let's add a start script to our package.json.
"scripts": {
"start": "parcel ./src/index.html"
}
When we run yarn start
, parcel should go to work processing our files. When it's finished, it will start a server, usually at localhost:1234. With not-too-much work, we've got React up and running. We can even import CSS as normal; go ahead an add the following to style.css
. Don't worry about stopping and restarting the server, Parcel watches your project and even hot-reloads components that change.
#app{
color: blue;
}
Add import "./style.css"
after the React
import in App.js
. Your text should now change color.
Now, for the 'fun' part: The word reveal component. This is a functional component that uses a React state hook to keep track of what character indexes have been revealed, so we can show them one-by-one, in order. Add the following to WordReveal.js
import React, { useState } from "react";
const WordReveal = ({ str }) => {
const [wordIndex, setWordIndex] = useState(-1);
return (
<div>
<p>
Click the button to reveal the next character of the secret string.
</p>
<div>
{str.split("").map((character, index) => {
if (index > wordIndex)
return <span key={`${character}${index}`}>_</span>;
return <span>{character}</span>;
})}
</div>
<button onClick={() => setWordIndex(wordIndex + 1)}>
Next Character
</button>
<button onClick={() => setWordIndex(-1)}>Reset</button>
</div>
);
};
export default WordReveal;
The first line in our component function is a hook. We're using array destructuring here in order to get our value (as the first array element) and a function to set that value (as the second array element) as nicely named variables. We then set the initial value with useState()
. This is how React hooks are set up, at least for state. The component itself is otherwise standard fare until you reach the buttons, which utilize hooks as well. Both of them modify the wordIndex
, incrementing or resetting it respectively. Some of you who like fancy syntax such as ternaries may notice we can use that to determine which span
to show, but we're keeping it simple for now.
Without hooks, we would either have to feed in the index to the component, or we'd have to make it a class component. Since hooks use destructuring to get and set our values, we don't even need to worry about additional configuration beyond having the correct React version and a recently updated browser.
Let's go back to App.js
and use our WordReveal component. First, import it:
import WordReveal from "./WordReveal";
Then replace our original render function with the following
render() {
return (
<main>
<WordReveal str="NotSoSecret" />
</main>
);
}
You should now see the app rendering the WordReveal component! If you click the Next Character
button, you should see letters appear until you reach the end. You can click the other button to reset the index and hide the word once again.
Parcel isn't perfect, though. Let's try adding something fancier to our App.js
: a class property. Specifically, some state. Stop Parcel. Add the following to App.js
, before the render function.
state = {
yourName: "Jedai"
};
While we're at it, go ahead add an <h1>
element, as the first inside of the <main>
element in App.
<h1>Hello {this.state.yourName}</h1>
Start Parcel. It should throw an error when trying to build the project now.
Support for the experimental syntax 'classProperties' isn't currently enabled
In order for this to work, you'll need to get the proper Babel plugin. Specifically, we need @babel/plugin-proposal-class-properties
(docs) . We also need [@babel/core](https://babeljs.io/docs/en/babel-core)
. Let's get that with the following line.
yarn add -D @babel/core@7.7.0 @babel/plugin-proposal-class-properties@7.7.0
The next step is to create a .babelrc
file at the root of the project, and use the plugin we've added. This file determines which plugins will be used by Babel. Parcel also has its own .babelrc
that's been making everything work in the background up until now. Adding our own will make Parcel consider our plugins and presets as well as whatever it already has configured. Put the following in .babelrc
.
{
"plugins": ["@babel/plugin-proposal-class-properties"]
}
You should now see a greeting with your name (or mine, if you didn't bother to change it) before the WordRevealer.
Let's add one more function to our App, to illustrate how Parcel may fail without further configuration. Add the following after the state
declaration.
async asyncFunction() {
await true;
return;
}
This function doesn't do anything, but you'll notice adding it causes an error. Specifically, in the browser, you should see the following error:
Uncaught ReferenceError: regeneratorRuntime is not defined
Async/Await has pretty broad support now, but this time last year, it was still missing from some heavily-used browsers. Because of this, functions using Async/Await needed to be processed to be usable across as many browsers as possible. If we're planning for our app to be used on modern browsers, we can add some configuration to our package.json
that Parcel will use to determine what does and doesn't need to be transformed.
To do that, we'll add a browserslist
section to our package.json
"browserslist": [
"last 1 Chrome versions"
]
In this case, we're just targeting the most recent Chrome version which definitely implements Async/Await. At this time, most browsers support Async/Await, so feel free to play with which value you use. Your project should run again, as long as you target browsers that implement the features you're planning to use. You may need to refresh the page for the changes to take effect.
We could have also used Babel's runtime library, and if we were targeting a browser like IE, we would have to at this time. In our case, we just needed to add a bit more configuration and Parcel used that to determine what syntax to transform.
As you add more features to your app, you may need to seek out the right plugins or your answer might just be tweaking existing configuration. When I run into something that Parcel doesn't seem to have covered, I normally try to check the docs first, the Parcel repo issues second, and finally seek out a plugin if I haven't yet found an answer. I generally try to avoid adding dependencies unless everything else has failed.
There will likely come a time when you're ready to make your app live. For that, we're going to add another script to our package.json
:
"build": "parcel build ./src/index.html"
Now go ahead an build the project.
yarn run build
You might have noticed by now that Parcel also creates directories for the cache and distributable files. By default, these are .cache
and dist
. dist
is where your distributable files are located, whether you explicitly run a build or run the project in development. If you've done a few builds, you may also notice that it creates new files each time and doesn't delete the old ones. How you choose to handle that or what you do you do with those excess files is up to you, but out of the scope of this tutorial. What matters is that index.html
will refer to the latest bundle created.
In order to test our final bundled app, we'll need to 'properly' serve the page. We're going to use http-server to achieve this, without having to install any more dependecies.
npx http-server ./dist/
You should now be able to follow one of the provided links to see the app in action. That's it. I hope this tutorial has helped you grasp any one of the aformentioned libraries a tiny bit more. I also hope that it helps cut down on your development time if you're a person who doesn't tend to use tools like create-react-app
.
You can find a repo with the code for all of this here.
That's all from me, for now. Build great things. Do no harm with what you create.