ToDo List Ethereum Dapp (Step6) | Webpack, ES6 and truffle-solidity-loader

This tutorial is part of the series ToDo List Ethereum Dapp. This is the Step 6. Tutorials already published:

In this tutorial we are going to add webpack and babel to be able to use Javascript ES6 features in the browser. Let’s get started!

Setup project

As usual, you just need to copy and paste the folder of the last episode (step5) and rename it to step6:

cp -r special-episode-1/step5 special-episode-1/step6

Step into step6 folder and install npm dependencies:

cd step6
npm install

Awesome! You are ready to get started.

Install webpack, babel and truffle-solidity-loader

Install the required dev npm packages with this command:

npm install -D webpack webpack-cli babel-core@6 babel-loader@7 babel-preset-env truffle-solidity-loader json-loader

Note: if later in this tutorial you have problems with babel not working properly, it might be due to incompatible versions between babel-core and babel-loader. In this case just remove your node_modules folder, copy paste the package.json and package-lock.json of the GitHub repo of EatTheBlocks (inside special-episode-1 folder), and run npm install

Let’s now move on to configure babel by creating a file called .babelrc at the root of your project and add this inside:

{
  "presets": ["env"]
}

Then Configure webpack by creating another file called webpack.config.js at the root of your project as well. Inside copy paste this:

const path = require('path');

module.exports = {
  entry: './app/js/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'app/dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  }
};

It tells webpack to:

  • Take app/js/index.js as an input
  • Use babel to transform all the ES6 syntax to ES5 so that all browsers can understand it
  • Save the output file into app/dist/bundle.js

Now let’s update the <script> tag that loads our javascript code. In app/index.html:

Change this:

<script src="js/index.js"></script> 

To this:

<script src="dist/bundle.js"></script> 

As you can see, we are now loading the bundle created by webpack instead of the source file we wrote ourselves.

Setup new npm tasks

Now that we setup webpack and babel, we can use any ES6 features in our front code, without being afraid of compatibility issues with browsers. Babel will just transform everything into old-school ES5 Javascript that all browsers understand.

However, at the moment things are still very inconvenient. Every time we change any javascript file, we need to manually ask webpack to recompile our code. What would be great is a command that watch any file changes and run webpack automatically when needed.

Fortunately webpack already have this feature when you launch it with the --watch command:

webpack --watch

For our convenience, let’s create a npm command in our package.json file. Under the scripts key, add the dev-front command:

...,
"scripts": {
  "dev-front": "node_modules/.bin/webpack --watch",
   ...
},
...

We will also add another task to watch the backend code. First install nodemon with npm install -g nodemon, then add it to the package.json:

...,
"scripts": {
  "dev-back": "nodemon server.js",
   ...
},
...

Now let’s check that our setup works properly:

  • Run npm run dev-back
  • In another terminal window, run npm run dev-front
  • Open app/js/index.js, make a dummy change and save
  • In the terminal where you run npm run dev-front, you should see webpack recompiling everything. The frontend still need to be reloaded manually though (i.e we haven’t setup hot reloading)

Congrats, you just automated an important part of your workflow! Plus now you are free to use the latest Javascript features without fear!

Load smart contract artifact dynamically

So far, the way we load the smart contract in app/js/index.js is easy to understand but very impractical: every time we make a change to the smart contract, we need to copy paste the compiled artifact (build/contract/ToDo.json) into app/js/index.js. That’s ok when you are just trying things out but if you actually count on developing a dapp like this you will quickly loose your sanity!

Fortunately for us, the good people of the Truffle framework created a nifty npm package called truffle-solidity-loader that allow us to dynamically load our smart contract from the frontend, WITHOUT having to copy paste the compiled artifact every time we make a change to the smart contract.

NOTE: if you google Truffle Solidity Loader the first link that comes up is the OLD stand-alone repo. The docs are outdated, don’t follow it. The up-to-date repo is this one.

truffle-solidity-loader integrates perfectly with the webpack watch system. Every time you change the smart contract, webpack will:

  • Detect the change
  • Recompile the Solidity smart contract,
  • Re-run your Truffle migrations
  • Re-load the compiled artifact into app/js/index.js

All you need to do is add this entry to the module.rule array of webpack.config,js, after the first rule we already put just before:

...,
module: {
  rules: [
    ...,
    {
      test: /\.sol/,                                                                                                                                               
      use: [                                                                  
        {                                                                     
          loader: 'json-loader'                                               
        },                                                                    
        {                                                                     
          loader: 'truffle-solidity-loader',                                  
          options: {                                                          
            network: 'development',                                           
            migrations_directory: path.resolve(__dirname, './migrations'),    
            contracts_build_directory: path.resolve(__dirname, '../build/contracts')
           }                                                                   
         }                                                                     
       ]                                                                       
     }   
   ]
 }
...

In app/js/index.js, remove the compiled artifact of the smart contract. (It’s the huge JSON object of several thousand lines that start at the top of file and look like this):

 artifact = {                                                                                                                                                         
   "contractName": "ToDo",                                                       
   "abi": [                                                                      
    {                                                                           
      "inputs": [],                                                             
      "payable": false,                                                         
      "stateMutability": "nonpayable",                                          
      "type": "constructor"                                                     
    },
...

Finally add this to load the compiled artifact using ES6 features and truffle-solidity-loader:

import artifact from '../../contracts/ToDo.sol`;

Note that this time we don’t load the artifact from build/contracts/ToDo.json, but directly from the smart contract source. This save us from having to recompile manually after each change in the smart contract.

Modernize our code with ES6 features

Now that we can use ES6 features, we don’t need to go through the pain of copying over the browser-compatible versions of web3 and truffle-contract (respectively web3.min.js and truffle-contract.min.js).

Add these lines at the top of app/js/index.js, above the import we just added before:

import Web3 from 'web3';
import TruffleContract from 'truffle-contract';
...

Now you can delete unused script tags in app/index.html:

<script src="vendor/web3.min.js"></script>                                  
<script src="vendor/truffle-contract.min.js"></script>   

As well as the app/js/vendor folder:

rm -rf app/js/vendor

Back in app/js/index.js, let’s prefix variable declarations with the const keywords:

const web3 = new Web3(new Web3.providers.HttpProvider('http:localhost:9545');


const abstraction = new TruffleContract(artifact);


const network = Object.keys(artifact.networks)[0];                              
const address = artifact.networks[network].address; 

Connect to different Ethereum clients depending on environments

In this tutorial I would like to make you test the dapp with the stand-alone ganache-cli, not the one provided by Truffle when you run truffle develop on port 9545. I could just ask you to update this configuration in app/index.js:

const web3 = new Web3(new Web3.providers.HttpProvider('http:localhost:9545');

But that’s not very flexible. We potentially will have to change this often. We need a better way of handling this. The answer is to define an environment variable, and have our dapp connect to different Ethereum clients depending on its value.

In package.json, let’s prefix the dev-front command with an ENV variable:

...
"scripts": {                                                                  
  "dev-front": "ENV=development node_modules/.bin/webpack --watch", 
   ...
},
 ...

Now, open webpack.config.js and add this snippet above module.exports = ...:

 const envVariables = new webpack.DefinePlugin({                                 
   ENV: JSON.stringify(process.env.ENV)                                                                                                                                 
  });

Below the module entry, add a plugin array:

module: {
  ...
},
plugins: [
  envVariables
],
...

In the solidity-truffle-loader rule let’s also make the network value dynamic by replacing the static development string by the ENV environment variable:

...
options: { 
  network: process.env.ENV,
  ...

Now the content of the ENV environment variable is available in our frontend code.

Let’s create a new file called app/js/config.js whose job would be to determine what is the url of the Ethereum client to connect to, depending on ENV. Copy paste this in this file:

import truffle from '../../truffle.js';                                                                                                                              

const getEthereumUrl = (env) => {                                               
  const network = truffle.networks[env];                                        
  return `http://${network.host}:${network.port}`;                              
};                                                                              

const config = {                                                                
  ethereumUrl: getEthereumUrl(ENV),                                             
};                                                                              

export default config;  

What’s happening here? First, we import the configuration of truffle, where we define per environment the different ethereum clients we might want to connect to.

Then, getEthereumUrl() builds the url to connect to an Ethereum client from the truffle.js config and the environment.

Finally, we store the result of this in a config object that will be available to other modules. Notice how we simply reference the variable ENV that was populated by webpack.

We just have final thing to do before finishing this. We need to actually use the config in the main javascript file. Open app/js/index.js and import the config file below the TruffleContract import:

...
import config from './config.js';
...

And in the web3 configuration replace the static url string by `config.ethereumUrl`:

… const web3 = new Web3(new Web3.providers.HttpProvider(config.ethereumUrl)); … `\

Final test

Phew! Almost there. We just need to test that everything works fine. Follow these steps:

  • Run ganache-cli
  • In a new terminal, run npm run dev-back
  • In a new terminal, run npm run dev-front
  • In your browser, visit http://localhost:3000 and make sure that the dev console shows an output like this:

    (10) [“0xf76f2ab7c721ffcb953fe1a9d2110b14b931aa6e”, “0x005dcf583f7122397ac82cd9b99b2c14565dbf57”, “0x38e1a65b56d82f272aeb96c1c392195419097933”, “0x7c7185b77b453f2b3f27f56d669745b23399e383”, “0x8bab8cfa86db6c96fc65c35aa5556e78f28a940a”, “0x11483684b370d6a74a9b63b34b454134e0175531”, “0xdcf5346ab92ea40666a199d42a1ca7cb24aeb755”, “0xb2e5872f454bfd94d672b7312ce8ea377f8f76c5”, “0xb590c9a580afbb6d7157f63bcc85ba9826a5c28f”, “0xfaa46e6d2e14c4201e5ecbbd2d6b0e1256111f10”] bundle.js:8 []

If you change anything to contracts/ToDo.sol and save the file, webpack will enter in action to repackage everything, and when you reload the frontend everything still works with the updated smart contract address. No need to migrate, copy paste new contract artifact.

What a time saving!

The End

Congratulations! You just learned how to use the latest ES6 / webpack technologies to build a modern Dapp frontend. Also, thanks to truffle-solidity-loader your workflow has been simplified and became way faster.

In the next tutorial, we will read data from the frontend. Stay tuned!

Leave a Reply

Your email address will not be published. Required fields are marked *