Storing Data on IPFS using Web3.Storage!

Storing Data on IPFS using Web3.Storage!

If you're ever wondered how to store your website data in a decentralised way...

This article is part of my 'web 3' series where I share what I'm learning in #30DaysOfWeb3 - a 30-day online course by Women Build Web3!

If you are new to web3 - my Web 3 for Beginners article was featured on Hashnode and may be a good starting point for you :)

Introduction

So, you're a developer wanting to get into the web3 space - but maybe you're wondering how exactly you're going to store your data? This article should help.

Today, we are going to create this simple form below with React.js and store the input data on IPFS using Web3.Storage:

Screen Shot 2022-08-12 at 9.14.08 pm.png

What is Web3.Storage?

Screen Shot 2022-08-12 at 7.56.10 pm.png

Web3.Storage is a service created by Protocol Labs to help developers interact more easily with IPFS (InterPlanetary File System) and Filecoin.

Some liken IPFS to the harddrive of blockchain and web3.

But why do we need Filecoin if we have IPFS?

Eventhough IPFS is decentralized, it is not a blockchain.

As such, data persistence is not guaranteed on IPFS - unless you run your own IPFS node or pay another IPFS node to pin your data. This is where Filecoin comes in.

Filecoin (also created by Protocol Labs) is a decentralised storage market built on top of IPFS. It incentivises IPFS nodes to pin your data by paying them using FIL token - which is the token of Filecoin.

Web3.Storage (another Protocol Labs product) makes use of both IPFS and Filecoin. It basically gives you a free api - allowing you to both store your data on IPFS and also take advantage of the data persistence of Filecoin.

But how much does it cost? This is what Web3.Storage says about this:

Web3.Storage is designed to scale to meet your storage needs. The economics of Filecoin allow us to offer all storage for free today.

The 1 TiB storage tier is open to everyone. If you need more than 1TiB, that's still free today, but we need to know more about your project so we can scale the service to meet your needs.

In the future, users will be able to pay to increase their storage capacity. This will remain low-cost, and all limits already increased for free will continue to be respected!

Here's some more information about Web3.Storage:

From the Web3.Storage docs:

In the past, storing data on the decentralized web wasn't always easy — but that's where Web3.Storage comes in. Most data on the internet today is hosted by massive storage providers like Amazon, Google, and Microsoft. This makes it simpler for developers to store application data, but big corporate platforms like these create silos by locking developers and users alike into walled gardens of services....

...One solution to this problem is using decentralized storage instead of big, corporate platforms to store data for apps and services. However, decentralized storage can be difficult to manage and add extra time and effort to an already crowded developer workflow — for example, most decentralized storage services need you to compile your data into a specific format, find a storage provider to host your data, buy some cryptocurrency to pay the storage provider, and then send your data across the internet. This is where Web3.Storage comes in.

With Web3.Storage, you get all the benefits of decentralized storage technologies with the frictionless experience you expect in a modern dev workflow. All you need to use Web3.Storage is an API token and your data. Under the hood, Web3.Storage is backed by the provable storage of Filecoin and makes data accessible to your users over the public IPFS network — but when it comes down to building your next application, service, or website, all you need to know is that Web3.Storage makes building on decentralized technologies simple. And best of all, Web3.Storage is free.

There are some important things to note about Web3.Storage though before you start using it:

1) All data uploaded to Web3.Storage is available to anyone who requests it using the correct CID. Do not store any private or sensitive information in an unencrypted form using Web3.Storage.

2) Deleting files from the Web3.Storage site’s Files page will remove them from the file listing for your account, but that doesn’t prevent nodes on the decentralized storage network from retaining copies of the data indefinitely. Do not use Web3.Storage for data that may need to be permanently deleted in the future.

But why don't we store our data directly on the blockchain?

Put simply - it is just not cheap, nor efficient!

Purestorage.com puts it this way:

Blockchain is a good fit when you need a system of record wrapped in total security, validity, and traceability. But for storage of larger files and more associated metadata, underlying databases will still be critical.

In other words, blockchains are good for storing tiny transaction data - such as a monetary transaction - but not good for storing image files, for example!

A smart contract would usually handle a monetary transaction on the blockchain and we could index it from the blockchain using a protocol such as The Graph.

But for today, let's focus on Web3.Storage!

Let's get started!

Note: You can interact with Web3.Storage through the JavaScript Client Library (i.e. programmatically through your website code), or you can upload your files in your Web3.Storage account directly.

In this tutorial, we are going to use our React.js code to interact with our Web3.Storage account.

1) Sign up on Web3.Storage

Sign up on Web3.Storage and generate your free Web3.Storage API token on your accounts page.

Screen Shot 2022-08-13 at 9.45.19 am.png

2) Store your api token in a .env.local file

Create a .env.local file in your React project and add your Web3.Storage api token like this:

REACT_APP_WEB3STORAGE_TOKEN=<Your API token>

Storing this token in a .env.local file means that your website users can't access it on the front-end.

Don't share your api token with anyone!

NOTE: The naming of this token in the .env.local file would be different if we were say, using Next.js - it would be `NEXT_PUBLIC_WEB3STORAGE_TOKEN".

3) Install Web3.Storage in your project

npm i web3.storage

Make sure you are using the correct version of Node.js and npm.

You should now see Web3.Storage in your dependencies in your package.json file:

Screen Shot 2022-08-13 at 12.00.39 pm.png

4) Set up your form component in your React.js project

Create your component with your desired JSX structure.

I am using Tailwind CSS for this form - it is a utility-first CSS framework. You can also add it to your project by following their docs.

So, your JSX code could perhaps look like this:

 return (
    <div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
      <header className="text-center">
        <h1 className="text-xl pt-12 pb-0">
          {" "}
          Store React.js Form Data in Web3
        </h1>
      </header>
      <main>
        <section className="relative py-12">
          <form
            onSubmit={handleSubmit}
            className="space-y-8 divide-y divide-gray-200"
          >

            <div className="space-y-6 sm:space-y-5">
              <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
                <label
                  htmlFor="eventname"
                  className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
                >
                  Name
                </label>
                <div className="mt-1 sm:mt-0 sm:col-span-2">
                  <input
                    id="event-name"
                    name="event-name"
                    type="text"
                    className="block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md"
                    required
                    value={username}
                  />
                </div>
              </div>

              <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
                <label
                  htmlFor="event-link"
                  className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
                >
                  Link
                </label>
                <div className="mt-1 sm:mt-0 sm:col-span-2">
                  <input
                    id="event-link"
                    name="event-link"
                    type="text"
                    className="block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md"
                    required
                    value={link}
                  />
                </div>
              </div>

              <div className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
                <label
                  htmlFor="event-link"
                  className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2"
                >
                  Image
                  <p className="mt-1 max-w-2xl text-sm text-gray-400">
                    Upload an image
                  </p>
                </label>
                <div className="mt-1 sm:mt-0 sm:col-span-2">
                  <input
                    id="event-image"
                    name="event-image"
                    type="file"
                    className="block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md"
                    required
                  />
                </div>
              </div>
            </div>

            <div className="flex justify-end">
              <button
                type="submit"
                className="ml-3 mt-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-full text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Submit
              </button>
            </div>
          </form>
        </section>
      </main>
    </div>
  );

In the above code, you will notice that for the image uploader, the type of input is file, like this:

<input type="file"/>

You will also need a button of type="submit" at the end of your form.

5) Create a state variable for each input element

This should look something like this:

  const [username, setUsername] = useState("");
  const [link, setLink] = useState("");
  const [image, setImage] = useState(null);

You will see that we have initialised our first two state variables with empty strings and we have initialised our image state variable with null.

Don't forget to import the useState hook at the top of your file.

import { useState } from "react"

6) Add an 'onChange' property on each of the input elements

This onChange function will look something like this:

<input onChange={(e) => setUserName(e.target.value)} />

However, you will notice that for our image uploader input, we don't update our state variable with e.target.value - we instead update it with e.target.files[0].

<input onChange={(e) => setImage(e.target.files[0])}/>

Why do we do this? This is because we only want to access the first item in the files array (because we are only allowing the user to upload one image for this form), but the files array has the ability to hold multiple images.

7) Add a value attribute to each input element

Use your state variables to add value attributes to each input element.

<input value={username} />

Why do we do this?

Basically, we want our component to be a controlled component - i.e. we want to be sure that the input's value is always driven by the state.

Note: In React, an <input type="file" /> is always an uncontrolled component because its value can only be set by a user, not programmatically.

8) Add an onSubmit event handler to the form element

You can now go ahead and pass a handleSubmit function to the onSubmit event handler which will be triggered each time the form is submitted.

<form onSubmit={handleSubmit}></form>

9) Write the body of your handleSubmit function:

Let's create our handleSubmit function. It will be an asynchronous function:

a) The first thing we're going to do is use the e.preventDefault() method to stop the form from reloading.

 async function handleSubmit(e) {
    e.preventDefault();
}

b) Next, when the user clicks the form button, we want to store our state variables in an object.

Add the code below to your handleSubmit function.

    const body = {
      name: username,
      link: link,
      image: image.name,
    };

This is the data that we will be storing on Web3.Storage - we will call this object: body.

Note: For the image, we want to access image.name from the image state variable - which is of File data type. File data type always contains the name of the file that you are uploading.

10) Create a makeStorageClient function

Let's leave our handleSubmit function for a bit and create a makeStorageClient function just below our imports at the top of file (i.e. outside of the function component).

This function will allow us to create a new instance of the Web3Storage object which will contain our API token.

This Web3Storage object will allow us to interact with Web3.Storage.

function makeStorageClient() {
  return new Web3Storage({
    token: process.env.REACT_APP_WEB3STORAGE_TOKEN,
  });
}

You will see in the above code that we are not directly using our Web3.Storage token, but rather referencing it from our .env.local file.

Also, don't forget to import Web3.Storage at the top of the file, like this:

import { Web3Storage } from "web3.storage";

11) Add a try/catch/finally statement to your handleSubmit function

Going back to our handleSubmit function, you can add the code below:

  try {
      const buffer = Buffer.from(JSON.stringify(body));
      const files = [new File([buffer], "data.json"), image];
      const client = makeStorageClient();
      const cid = await client.put(files);
      console.log(cid)
      await createEvent(cid); /// Optional - passing CID to a function that will interact with a smart contract to         use that data.
    } catch (error) {
      alert(
        `Oops! Something went wrong. Please refresh and try again. Error ${error}`
      );
    } finally {
   setUsername("")
   setLink("")
   setImage(null)
}

Let's go a bit deeper into what is happening here.

1) const buffer = Buffer.from(JSON.stringify(body));

Here we are converting the body object into a Buffer module. The buffer module provides a way of handling streams of binary data.

2) const files = [new File([buffer], "data.json"), image];

In this line, we are converting buffer into a File object.

The File() constructor creates a new File object instance. It accepts an array of two file objects - one with the buffer data (which we will name data.json) and also the image file which is our state variable.

We do this step in order to convert our data into the correct format that Web3.Storage requires.

The Web3.Storage client's 'put' method requires an array of Files

NOTE: If you are getting an error that Buffer is not defined, do this:

a) Install Buffer

npm i buffer

b) Add this code to the top of your component file:

import { Buffer } from "buffer";

window.Buffer = Buffer;

3) const client = makeStorageClient();

Here we are creating a client variable that is calling the makeStorageClient() function. We created this makeStorageClient function earlier.

4) const cid = await client.put(files)

In the above line of code, we are creating a variable called cid and uploading our files with the help of the client's put method. In return, we will get the CID.

The CID (i.e. the content identifier) is a unique identifier on IPFS - it is like the url to your file. It is represented by a hash - a long string of letters and numbers.

So each time your form is submitted, a new CID will be created for that set of data.

5) OPTIONAL: await createEvent(cid);

This line is optional for the purposes of this article.

Here we are passing the CID to a function that will interact with our smart contract (if we have one) to use that data elsewhere on your website/DApp.

Below is an example of passing your CID to a function that will be passed to your smart contract. You would include this within your handleSubmit function.

 async function createEvent(cid) {
      try {
        const rsvpContract = connectContract();

        if (rsvpContract) {
          let deposit = ethers.utils.parseEther(refund);
          let eventDateAndTime = new Date(`${eventDate} ${eventTime}`);
          let eventTimestamp = eventDateAndTime.getTime();
          let eventDataCID = cid;

          const txn = await rsvpContract.createNewEvent(
            eventTimestamp,
            deposit,
            maxCapacity,
            eventDataCID,
            { gasLimit: 900000 }
          );
          setLoading(true);
          console.log("Minting...", txn.hash);
          let wait = await txn.wait();
          console.log("Minted -- ", txn.hash);

          setEventID(wait.events[0].args[0]);

          setSuccess(true);
          setLoading(false);
          setMessage("Your event has been created successfully.");
        } else {
          console.log("Error getting contract.");
        }
      } catch (error) {
        setSuccess(false);
        setMessage(`There was an error creating your event: ${error.message}`);
        setLoading(false);
        console.log(error);
      }
    }
    console.log("Form submitted");
  }

6) Clear your state variables

At the end of your try/catch/finally statement, be sure to reset your state variables, like this:

    } finally {
   setUsername("")
   setLink("")
   setImage(null)
}

12) View the data in your browser

To now see your data on Web3.Storage, fill out your form and push submit.

We have added a 'console.log(cid)' statement in the 'try/catch/finally' statement, so that you can see the CID in your console:

Screen Shot 2022-08-13 at 12.31.09 pm.png

Copy the CID that you see in your console.

Then, type this into your browser:

Be sure to insert your CID!

https://<CID>.ipfs.dweb.link/

When you do this, you should see your data.json file and your image file. It will look something like this.

Screen Shot 2022-08-13 at 10.48.56 am.png

Congrats! You have just stored your React form data on Web3.Storage! 🥳

You can even share this IPFS link so that others can download the files if they wish to!

But a reminder - everything you store on Web3.Storage is publicly available - so don't add sensitive files!

13) Retrieve your data

Now that you have stored your data, there are many ways to retrieve it. You may want to insert it into the UI of another part of your website, or use it for a completely separate website altogether!

Head over to the Web3.Storage docs to learn more about this.

Conclusion

Storing data on the blockchain is not cheap or efficient! Web3.Storage provides a free and easy way to store your website data in a decentralised way.

I hope that you found this helpful! To see my Github Repo for this tutorial - click here.

If you'd like to also add a 'Connect Wallet' button to allow your website visitors to do monetary transactions that are stored on the blockchain (such as paying for a service), take a look at my previous articles:

Add a 'Connect Wallet' Button to Your Website - Vanilla JS Edition!

Add a 'Connect Wallet' Button to Your Website - Next.js and RainbowKit Edition!

Happy Coding! 💜

Follow me on Twitter to see my progress with #30DaysOfWeb3 by @womenbuildweb3!

Did you find this article valuable?

Support Hayley is Coding 🦋 by becoming a sponsor. Any amount is appreciated!