Building a web frontend
The Internet Computer allows you to host frontends built with standard web technologies for your dApps, using
our JavaScript agent as a communication layer. By using
the asset canister provided by dfx
to upload static files to the IC,
you will be able to run your entire application on decentralized technology. This section takes a closer look at the
default frontend template that is provided by dfx new
, frontend configuration options, and using other frameworks to
build the user interface for your projects.
Here are some quick links to tutorials with example code for various stages of developing your frontend dapp:
A tutorial on building a React dapp Customize the frontend
Using Candid as a bare-bones interface to expose and test the functions in a canister.
Using raw HTML and JavaScript to display a simple HTML entry page.
Using React and compiled JavaScript to embed HTML attributes and elements directly in a page.
Using React and TypeScript to import CSS properties from an external file.
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 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 use the code under declarations
without any changes, 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 frontend or Add a stylesheet frontend 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 frontend, 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 thedfx.json
file or build your project using a specific canister name. For example, you can choose to build only the hello program without frontend 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 runningnpm install
in the project directory then re-runningdfx build
.If running
npm install
in the project directory doesn’t fix the issue, you should check the configuration of thewebpack.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
:
Create a new project and change to your project directory.
Start the IC locally, if necessary, and deploy as you normally would, for example, by running the
dfx deploy
command.Start the webpack development server by running the following command:
npm start
Open a web browser and navigate to the asset canister for your application using port 4943.
For example:
http://localhost:4943
Open a new terminal window or tab and navigate to your project directory.
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);
}Save your changes to the
index.js
file but leave the editor open to continue making changes.Refresh the browser or wait for it to refresh on its own to see your change.
When you are done working on the frontend 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:
Remove the
copy:types
,prestart
, andprebuild
scripts frompackage.json
Run
dfx deploy
to generate the local bindings for your canistersCopy the generated bindings to a directory where you would like to keep them
Modify
declarations/<canister_name>/index.js
and replaceprocess.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
Commit the declarations and import them in your codebase