Create a Decentralized App Using React and Juno
Written on
Introduction to Building Web3 Applications
In the realm of Web3 development, various frameworks offer distinct advantages and challenges, particularly in areas like wallet connectivity and transaction management. For frontend JavaScript developers venturing into the decentralized web, the development landscape can feel quite different compared to the more familiar Web2 environment.
Juno aims to bridge this gap, offering a user-friendly approach that leverages the strengths of Web3 while maintaining the simplicity and comfort of Web2 development practices. In this guide, we will delve into how to utilize React alongside Juno to create a decentralized application (dApp). Letโs explore how Juno can empower you to craft robust and intuitive decentralized apps!
Understanding Juno
Juno operates as an open-source Blockchain-as-a-Service platform. It functions similarly to conventional serverless platforms like Google Firebase or AWS Amplify, with the significant distinction that all operations on Juno are blockchain-based. This setup provides a fully decentralized and secure framework for your applications, which is a notable advantage.
Under the hood, Juno leverages the Internet Computer blockchain network to establish a "Satellite" for each application you develop. Essentially, a Satellite is an advanced smart contract that encompasses your entire application, including web assets such as JavaScript, HTML, and image files, along with its state stored in a simple database, file storage, and authentication. Each Satellite is uniquely controlled by you, ensuring it has everything necessary for seamless operation.
Getting Started with Your First dApp
Now, let's embark on creating our first decentralized app. For this tutorial, we will build a basic note-taking application that enables users to store data entries, upload files, and retrieve them as needed.
Initialization
To begin integrating Juno into your ReactJS application, you must first create a satellite. Detailed instructions for this process can be found in the documentation. Additionally, you will need to install the Juno SDK:
npm i @junobuild/core
Once these steps are completed, initialize Juno using your satellite ID at the top of your React application. This action will set up the library to connect with your smart contract:
import { useEffect } from "react";
import { initJuno } from "@junobuild/core";
function App() {
useEffect(() => {
(async () =>
await initJuno({
satelliteId: "pycrs-xiaaa-aaaal-ab6la-cai",}))();
}, []);
return (
Hello World);
}
export default App;
With this configuration, your application is now equipped for Web3! ๐
User Authentication
To ensure users can securely log in and out while maintaining anonymity, youโll need to implement the corresponding functions within your app. You can connect these to various call-to-action elements throughout your interface:
import { signIn, signOut } from "@junobuild/core";
// Login
// Logout
The library and satellite will automatically create a new entry in your smart contract upon a successful user login. This feature allows the library to verify permissions during data exchanges. To monitor this entry and track user state, Juno offers an observable function named authSubscribe(). It can be utilized multiple times, but itโs convenient to subscribe at the top of your app. This way, you can create a Context to propagate user information:
import { createContext, useEffect, useState } from "react";
import { authSubscribe } from "@junobuild/core";
export const AuthContext = createContext();
export const Auth = ({ children }) => {
const [user, setUser] = useState(undefined);
useEffect(() => {
const sub = authSubscribe((user) => setUser(user));
return () => unsubscribe();
}, []);
return (
{user !== undefined && user !== null ? (
{children}) : (
Not signed in.)}
);
};
Juno's library is designed to be framework-agnostic and currently does not contain any framework-specific code. However, community contributions for React plugins, contexts, and hooks are welcome! ๐ช
Document Storage
Storing data on the blockchain via Juno is facilitated through a feature known as "Datastore." A datastore consists of a series of collections, each containing documents identified by a unique key you define.
In this tutorial, our focus will be on storing notes. To do this, follow the documentation to create a collection named "notes." Once your app is set up and your collection established, you can persist data on the blockchain using the setDoc function:
import { setDoc } from "@junobuild/core";
// TypeScript example from the documentation
await setDoc({
collection: "my_collection_key",
doc: {
key: "my_document_key",
data: myExample,
},
});
Because documents within the collection are identified by unique keys, we will generate these keys using nanoid, a small string ID generator for JavaScript:
import { useState } from "react";
import { setDoc } from "@junobuild/core";
import { nanoid } from "nanoid";
export const Example = () => {
const [inputText, setInputText] = useState("");
const add = async () => {
await setDoc({
collection: "data",
doc: {
key: nanoid(),
data: {
text: inputText,},
},
});
};
return (
<>
setInputText(e.target.value)}
value={inputText}>
Add
);
};
Listing Documents
To retrieve the list of documents stored on the blockchain, you can employ the listDocs function from the library. This function allows for various parameters to filter, order, or paginate the data.
In this example, we will keep it simple and display all data belonging to the users while monitoring the previously declared Context. If a user is identified, we fetch the data; otherwise, we reset the entries:
import { useContext, useEffect, useState } from "react";
import { AuthContext } from "./Auth";
import { listDocs } from "@junobuild/core";
export const ListExample = () => {
const { user } = useContext(AuthContext);
const [items, setItems] = useState([]);
const list = async () => {
const { items } = await listDocs({
collection: "notes",
filter: {},
});
setItems(items);
};
useEffect(() => {
if ([undefined, null].includes(user)) {
setItems([]);
return;
}
(async () => await list())();
}, [user]);
return (
<>
{items.map(({ key, data: { text } }) => (
{text}))}
);
};
File Uploading
Storing user-generated content on the decentralized web can present challenges. Luckily, Juno simplifies this process, making it easy for developers to upload and serve assets like images or videos.
To upload files, you must create a collection as outlined in the documentation. For this tutorial, we will focus on uploading images, so the collection can be called "images." The stored data will be identified by unique file names and paths, as each piece of data should correspond to a unique URL.
To achieve this, we can generate a key by combining the userโs unique ID with a timestamp for each uploaded file. The property passed through the Context in the previous section will provide access to the relevant user key:
import { useContext, useState } from "react";
import { AuthContext } from "./Auth";
import { uploadFile } from "@junobuild/core";
export const UploadExample = () => {
const [file, setFile] = useState();
const [image, setImage] = useState();
const { user } = useContext(AuthContext);
const add = async () => {
const filename = ${user.key}-${file.name};
const { downloadUrl } = await uploadFile({
collection: "images",
data: file,
filename,
});
setImage(downloadUrl);
};
return (
<>
setFile(event.target.files?.[0])}/>
Add
{image !== undefined && }
);
};
Once an asset is successfully uploaded, a downloadUrl will be returned, providing a direct HTTPS link to access the uploaded asset on the web.
Listing Uploaded Assets
To retrieve the list of assets stored on the blockchain, utilize the listAssets function from the library. Like the document listing, this function can accept parameters for filtering, ordering, or paginating the files.
For simplicity, we will list all user assets while observing the Context:
import { useContext, useEffect, useState } from "react";
import { AuthContext } from "./Auth";
import { listAssets } from "@junobuild/core";
export const ListAssetsExample = () => {
const { user } = useContext(AuthContext);
const [assets, setAssets] = useState([]);
const list = async () => {
const { assets } = await listAssets({
collection: "images",
filter: {},
});
setAssets(assets);
};
useEffect(() => {
if ([undefined, null].includes(user)) {
setAssets([]);
return;
}
(async () => await list())();
}, [user]);
return (
<>
{assets.map(({ fullPath, downloadUrl }) => (
))}
);
};
Deploying Your App ๐
Once your application is fully developed, itโs time to deploy it on the blockchain. To do this, you need to install the Juno command line interface by running the following command in your terminal:
npm i -g @junobuild/cli
After installation, you can log in to your satellite from the terminal using the provided instructions in the documentation. This step will grant control of your machine to your satellite:
juno login
Finally, deploy your project with the following command:
juno deploy
Congratulations! Your application is now fully decentralized ๐.
Additional Resources
๐ Thank you for reading! Follow me on Twitter for more coding insights. If you've made it this far, consider joining Juno on Discord for community support. ๐