Build a Full Stack Dapp With Arweave, React, and GraphQL

Build a Full Stack Dapp With Arweave, React, and GraphQL

Make an Arweave photo storage app that anyone can use!

davidkong.blog · 9 minute read

In this article, we will walk through the process and code involved in building a simple full-stack photo storage app using Arweave, React, and GraphQL. I recommend already being familiar with React before you begin.

What is Arweave?

Arweave is an open, permissionless, and decentralized data storage protocol that lets you store your files FOREVER. As of January 2023, it costs less than $1 USD to store over 100 photos!

Many people are already using Arweave to preserve history, fight censorship, and store data as part of a fully decentralized web3 stack. I encourage you to read more about Arweave here.

What will we be building?

This project is called SnapShrine! SnapShrine lets anyone with an ArConnect wallet upload image files to Arweave. ArConnect is a browser wallet for Arweave, similar to what Metamask is for Ethereum. After a user connects their wallet, they can see all the photos they've uploaded, on the same page. You can see and try a working demo of this project here:

snapshrine.xyz

This front end uses GraphQL and Apollo client to query the official Arweave GraphQL endpoint, https://arweave.net/graphql.

What this means is that we pull data directly from Arweave so that we know which images the user has uploaded based on their wallet address, and then we use that data to show the images directly on the page. Pretty cool, eh?

If you get stuck while building, check out the full Github repo for reference.

As a side note, this project was inspired by an earlier project built by Nader Dabit, as well as existing Arweave dapps such as ArDrive and Akord.

Getting Started

This project uses Create React App. In your terminal, type the following:


npx create-react-app snapshrine
cd snapshrine

You are now in the root directory of your React project!

Installing Dependencies

Copy the package.json from the provided repo into your project, and make sure to install all necessary dependencies by running the following in your terminal:


npm install


This app uses react-dropzone to allow users to drag and drop image files directly onto the page. For querying data, it uses Apollo Client and GraphQL. It uses the arweave JS library to upload images directly to Arweave.

Also, there are three dev dependencies, webpack, util, and process. All three are necessary in order for the project to build successfully if you want to host a live version. After installing all dependencies, make sure to also copy webpack.config.js into your project's root directory.

App.js

At the top of App.js, you'll see the imports for this file. First, we have our main CSS file, App.css and standard React hooks, useState and useEffect. We also import the Dropzone and Gallery components, which we will work on later.


import "./App.css";
import { useState, useEffect } from "react";
import Dropzone from "./components/Dropzone.js";
import Gallery from "./components/Gallery";

Next, we have all the code that is relevant to the ArConnect Wallet. This includes two state variables:

walletConnected is a boolean that tells us whether or not the user has successfully connected their wallet.

walletAddress is the address of the wallet which is connected.

The permissions array is a list of rights the user is asked to grant to our website when they connect to their wallet.


const permissions = [ "ACCESS_ADDRESS", "SIGN_TRANSACTION", "SIGNATURE", "ACCESS_PUBLIC_KEY", "ACCESS_ALL_ADDRESSES", ];

async function connect() {

await window.arweaveWallet.connect(permissions);

setWalletConnected(true);

return; }

async function getWalletAddress() {

return await window.arweaveWallet.getActiveAddress(); }

useEffect(() => {

if (walletConnected) {

getWalletAddress().then((address) => {

setWalletAddress(address);

}); }

}, [walletConnected]);


Anytime you see window.arweaveWallet in the code, know that it's coming from the installed ArConnect wallet in the browser. You don't need to install anything via npm to have access to arweaveWallet.

The connect and getWalletAddress functions are very straightforward and do what their names suggest. The function inside useEffect is what allows us to store the user's wallet address in the state variable so that we can use it later to query data and also display the address on a button.

The last part of App.js renders our JSX, and uses a ternary operator to determine what to show based on if the user's wallet is connected or not. You'll also see that we are rendering the Dropzone and Gallery components, which we will build next.


return ( <div className="container">

<h3>Your Permaweb Photo Album, Powered by Arweave</h3> {walletConnected ? (

<div className="connectedButton"> Connected to {walletAddress.substring(0, 6)}... </div> ) : (

<button className="button" onClick={connect}> Use Arconnect Wallet </button> )}

<Dropzone />

<Gallery walletAddress={walletAddress} />

<h2> Note: In order to use this app, you must have{" "}

<a href="https://www.arconnect.io/" target="_blank"> Arconnect Wallet </a>{" "} installed and funded with some{" "} <a href="https://faucet.arweave.net/" target="_blank"> AR token </a> .{" "} </h2>

<h2> View code on{" "} <a href="https://github.com/sdavidkong/snapshrine" target="_blank"> Github </a> </h2> </div> ); }

export default App;


Dropzone.js

Within your 'src' folder, create a new folder called 'components'. Within components, create two files, named Dropzone.js and Gallery.js. We'll begin working on Dropzone first.


import React, { useState, useCallback } from "react";

import Arweave from "arweave";

import { useDropzone } from "react-dropzone";

import "../App.css";

const arweave = Arweave.init({

host: "arweave.net",

port: 443,

protocol: "https",

});


Once again, we import some hooks from React, including useState and useCallback as well as our CSS file. We also import the arweave js and react-dropzone libraries. In order for our front end to interact with Arweave, we must initialize it by specifying a host, port, and protocol.


const Dropzone = () => {

const [file, setFile] = useState(null);

const { getRootProps, getInputProps } = useDropzone({

accept: "image/*",

onDrop: (acceptedFiles) => {

if (acceptedFiles.length === 1) {

setFile(acceptedFiles[0]);

} else {

alert("Please drop only 1 file at a time.");

} },

});

const handleUpload = useCallback(async () => {

if (file) {

const reader = new FileReader();

reader.readAsArrayBuffer(file);

reader.onloadend = async () => {

const buffer = new Uint8Array(reader.result);

const transaction = await arweave.createTransaction({

data: buffer, });

transaction.addTag("Content-Type", "image/png");

await arweave.transactions.sign(transaction);

let uploader = await arweave.transactions.getUploader(transaction);

while (!uploader.isComplete) {

await uploader.uploadChunk();

console.log( `${uploader.pctComplete}% complete, ${uploader.uploadedChunks}/${uploader.totalChunks}` ); }

setFile(null);

alert( "Upload Successful. Please allow several minutes for transaction to finalize." ); }; }

}, [file]);


At the beginning of our Dropzone component, we specify that we are only allowing users to upload images and that they can only upload 1 image at a time. If the user tries to select another type of file or multiple files, they will get a pop-up alert.

Then, the handleUpload function defines the process for how our app takes the selected file and uploads it to Arweave. It will initiate a transaction that charges the user some small amount of $AR, and then resets the Dropzone so that the user can upload another image. The line transaction.addTag("Content-Type", "image/png") ensures that any image that is uploaded automatically has the image/png tag attached to it. Finally, it displays an "Upload Successful" alert once the transaction has gone through.


return (

<div>

{file ? (

<div className="dropzone">

<p>Filename: {file.name}</p>

<p>File Type: {file.type}</p>

<p>Size: {file.size} bytes</p>

<button className="button" onClick={handleUpload}>

Upload to Arweave

</button> </div>

) : (

<div className="dropzone" {...getRootProps()}>

<input {...getInputProps()} />

<p> Drag & Drop an Image Here <br></br> Or click to Select a File </p>

</div> )}

</div> ); };

export default Dropzone;


The last part of Dropzone.js renders the dropzone area on our page. When a file is selected or dropped, the component shows the user the file name, size, and type, and renders a "Upload to Arweave" button.

Gallery.js

Just like the Dropzone.js component, we are starting off in Gallery.js by importing and initializing Arweave. We also import Apollo Client and useState from React.


import { gql, useQuery } from "@apollo/client";

import Arweave from "arweave";

import "../App.css";

import React from "react";

const arweave = Arweave.init({

host: "arweave.net",

port: 443,

protocol: "https",

});


Then, we have the query used to get image data from Arweave. This query filters all transactions by the connected wallet address which also have the tags with a Content-Type of image/png. Because our Dropzone component adds these tags automatically, we know that the images will be returned from the query.


const GET_IMAGES = gql`

query ($walletAddress: String!) {

transactions(

owners: [$walletAddress]

tags: { name: "Content-Type", values: ["image/png"] }

) {

edges {

node {

id

}

} } } `;


Specifically, the query above returns an Arweave transaction ID, which is the node { id } section in the query. You can also search for the transaction ID in an Arweave block explorer to view the details of the transaction and the data in it. The transaction ID is crucial to creating a URL using the arweave.net gateway in order to display the image in the browser.

The last part of the Gallery component gets the wallet address of the user and constructs the image URLs from the IDs returned from the query.


const Gallery = (props) => {

const { loading, error, data } = useQuery(GET_IMAGES, {

variables: { walletAddress: props.walletAddress },

});

if (loading) return <p>Loading...</p>;

if (error) return <p>Error</p>;

return (

<div className="image-container">

{data.transactions.edges.map(({ node }) => (

<img

key={node.id}

src={`https://arweave.net/${node.id}`}

alt={node.id}

/>

))} </div>

); };

export default Gallery;


You'll notice that the walletAddress is passed to the Gallery component via props from App.js. This is how the wallet address of the user is available for us to use in the GraphQL query to Arweave. You can actually query Arweave using many other filters as well, I encourage you to explore the possibilities here.

One other thing to note is that in the near future, the preferred way of querying Arweave will likely be by using The Graph. As of Jan 2023, their support for Arweave is in beta, but it's likely that most queries will be done by using one of their subgraphs in the future.

Conclusion

Now that we've covered the main parts of our app, the only remaining tasks are to style the page to your liking and add any customizations you would like to! Feel free to use the App.css file in the GitHub repo. You can run the app locally by typing the following in your terminal:


npm start


I hope this was an easy and fun introduction to building with and querying Arweave!

If you have any questions about this code or implementing it, please contact me via Twitter @shuaidavidkong

general