Add front-end assets

The tutorials introduced a few basic approaches to building a front-end user interface for your projects. For example, the tutorials demonstrated:

This section takes a closer look at the default front-end template, front-end configuration options, and using other frameworks to build the user interface for your projects.

How the default templates are used

As you might have noticed in the tutorials, projects include template index.js and webpack.config.js files.

By default, the index.js file imports an agent that is located in src/declarations folder. That directory will be generated by dfx when you run dfx deploy, either locally or when deploying to the IC.

That generated code will look like this:

import { Actor, HttpAgent } from "@dfinity/agent";

// Imports candid interface
import { idlFactory } from './hello.did.js';
// CANISTER_ID is replaced by webpack based on node enviroment
export const canisterId = process.env.HELLO_CANISTER_ID;

/**
 *
 * @param {string | Principal} canisterId Canister ID of Agent
 * @param {{agentOptions?: import("@dfinity/agent").HttpAgentOptions; actorOptions?: import("@dfinity/agent").ActorConfig}} [options]
 * @return {import("@dfinity/agent").ActorSubclass<import("./hello.did.js")._SERVICE>}
 */
export const createActor = (canisterId, options) => {
  const agent = new HttpAgent({ ...options?.agentOptions });

  // Fetch root key for certificate validation during development
  if(process.env.NODE_ENV !== "production") agent.fetchRootKey();

  // Creates an actor with using the candid interface and the HttpAgent
  return Actor.createActor(idlFactory, {
    agent,
    canisterId,
    ...options?.actorOptions,
  });
};

/**
 * A ready-to-use agent for the hello canister
 * @type {import("@dfinity/agent").ActorSubclass<import("./hello.did.js")._SERVICE>}
 */
export const hello = createActor(canisterId);

Then, if you return to index.js, you can see see that it takes the generated actor, and uses it to make a call to the hello canister’s greet method:

import { hello } from "../../declarations/hello";

document.getElementById("clickMeBtn").addEventListener("click", async () => {
  const name = document.getElementById("name").value.toString();
  // Interact with hello actor, calling the greet method
  const greeting = await hello.greet(name);

  document.getElementById("greeting").innerText = greeting;
});

In many projects, you will be able to ignore the code under declarations for the most part, and make your changes in hello_assets/src. However, if your project has additional requirements, continue reading below.

Modifying the webpack configuration

Because webpack is a popular and highly-configurable module bundler for JavaScript-based applications, new projects create a default webpack.config.js file that makes it easy to add the specific modules—such as react and markdown—that you want to use.

If you review the code in the template webpack.config.js file, you see that it infers canister ID’s from your .dfx/local/canister_ids.json for local development, and from './canister_ids.json' for any other environments you configure. It decides which network to use based on a DFX_NETWORK proccess variable, or based on whether NODE_ENV is set to "production".

You can see these steps in the following code block:

let localCanisters, prodCanisters, canisters;

try {
  localCanisters = require(path.resolve(".dfx", "local", "canister_ids.json"));
} catch (error) {
  console.log("No local canister_ids.json found. Continuing production");
}

function initCanisterIds() {
  try {
    prodCanisters = require(path.resolve("canister_ids.json"));
  } catch (error) {
    console.log("No production canister_ids.json found. Continuing with local");
  }

  const network =
    process.env.DFX_NETWORK ||
    (process.env.NODE_ENV === "production" ? "ic" : "local");

  canisters = network === "local" ? localCanisters : prodCanisters;

  for (const canister in canisters) {
    process.env[canister.toUpperCase() + "_CANISTER_ID"] =
      canisters[canister][network];
  }
}
initCanisterIds();

Entry and output configuration

In many cases, you can use the default webpack.config.js file as-is, without any modification, or you can add plug-ins, modules, and other custom configuration to suit your needs. The specific changes you make to the webpack.config.js configuration largely depend on the other tools and frameworks you want to use.

For example, if you have experimented with the Customize the front-end or Add a stylesheet front-end tutorials, you might have modified the following section to work with React JavaScript:

    module: {
      rules: [
        { test: /\.(ts|tsx|jsx)$/, loader: "ts-loader" },
        { test: /\.css$/, use: ['style-loader','css-loader'] }
      ]
    }
  };
}

If your application does not use dfx to run your build script, you can provide the variables yourself. For example:

DFX_NETWORK=staging NODE_ENV=production HELLO_CANISTER_ID=rrkah... npm run build

Ensuring node is available in a project

Because projects rely on webpack to provide the framework for the default front-end, you must have node.js installed in your development environment and accessible in the project directory.

  • If you want to develop your project without using the default webpack configuration and canister aliases, you can remove the assets canister from the dfx.json file or build your project using a specific canister name. For example, you can choose to build only the hello program without front-end assets by running the following command:

    dfx build hello
  • If you are using the default webpack configuration and running dfx build fails, you should try running npm install in the project directory then re-running dfx build.

  • If running npm install in the project directory doesn’t fix the issue, you should check the configuration of the webpack.config.js file for syntax errors.

Using other modules with the React framework

Several tutorials and sample projects in the examples repository illustrate how to add React modules using the npm install command. You can use these modules to construct the user interface components you want to use in your project. For example, you might run the following command to install the react-router module:

npm install --save react react-router-dom

You could then use the module to construct a navigation component similar to the following:

import React from 'react';
import { NavLink } from 'react-router-dom';

const Navigation = () => {
  return (
    <nav className="main-nav">
      <ul>
        <li><NavLink to="/myphotos">Remember</NavLink></li>
        <li><NavLink to="/myvids">Watch</NavLink></li>
        <li><NavLink to="/audio">Listen</NavLink></li>
        <li><NavLink to="/articles">Read</NavLink></li>
        <li><NavLink to="/contribute">Write</NavLink></li>
      </ul>
    </nav>
  );
}

export default Navigation;

Iterate faster using webpack-dev-server

Starting with dfx 0.7.7, we now provide you with webpack dev-server in our dfx new starter.

The webpack development server—webpack-dev-server—provides in-memory access to the webpack assets, enabling you to make changes and see them reflected in the browser right away using live reloading.

To take advantage of the webpack-dev-server:

  1. Create a new project and change to your project directory.

  2. Start the Internet Computer locally, if necessary, and deploy as you normally would, for example, by running the dfx deploy command.

  3. Start the webpack development server by running the following command:

    npm start
  4. Open a web browser and navigate to the asset canister for your application using port 8080.

    For example:

    http://localhost:8080
  5. Open a new terminal window or tab and navigate to your project directory.

  6. Open the index.js file for your project in a text editor and make changes to the content.

    For example, you might add an element to the page using JavaScript:

    document.body.onload = addElement;

    document.body.onload = addElement;
    
    function addElement () {
      // create a new div element
      const newDiv = document.createElement("div");
    
      // and give it some content
      const newContent = document.createTextNode("Test live page reloading!");
    
      // add the text node to the newly created div
      newDiv.appendChild(newContent);
    
      // add the newly created element and its content into the DOM
      const currentDiv = document.getElementById("div1");
      document.body.insertBefore(newDiv, currentDiv);
    }
  7. Save your changes to the index.js file but leave the editor open to continue making changes.

  8. Refresh the browser or wait for it to refresh on its own to see your change.

    When you are done working on the front-end for your project, you can stop the webpack development server by pressing Control-C.

Using other frameworks

You may want to use a bundler other than webpack. Per-bundler instructions are not ready yet, but if you are familiar with your bundler, the following steps should get you going:

  1. Remove the copy:types, prestart, and prebuild scripts from package.json

  2. Run dfx deploy to generate the local bindings for your canisters

  3. Copy the generated bindings to a directory where you would like to keep them

  4. Modify declarations/<canister_name>/index.js and replace process.env.<CANISTER_NAME>_CANISTER_ID with the equivalent pattern for environment variables for your bundler

    • Alternately hardcode the canister ID if that is your preferred workflow

  5. Commit the declarations and import them in your codebase