js-waku/guides/reactjs-relay.md

7.6 KiB

Receive and Send Messages Using Waku Relay With ReactJS

It is easy to use DappConnect with ReactJS. In this guide, we will demonstrate how your ReactJS dApp can use Waku Relay to send and receive messages.

Before starting, you need to choose a Content Topic for your dApp. Check out the how to choose a content topic guide to learn more about content topics. For this guide, we are using a unique content topic: /min-js-web-chat/1/chat/proto.

Setup

Create a new react app:

npx create-react-app min-js-web-chat
cd min-js-web-chat

Then, install js-waku:

npm install js-waku

Start the dev server and open the dApp in your browser:

npm run start

Note: We have noticed some issues with React bundling due to npm pulling an old version of babel. If you are getting an error about the optional chaining (?.) character not being valid, try cleaning up and re-installing your dependencies:

rm -rf node_modules package-lock.json
npm install

Create Waku Instance

In order to interact with the Waku network, you first need a Waku instance. Go to App.js and modify the App function:

import { Waku } from 'js-waku';
import * as React from 'react';

function App() {
  const [waku, setWaku] = React.useState(undefined);
  const [wakuStatus, setWakuStatus] = React.useState('None');

  // Start Waku
  React.useEffect(() => {
    // If Waku is already assigned, the job is done
    if (!!waku) return;
    // If Waku status not None, it means we already started Waku 
    if (wakuStatus !== 'None') return;

    setWakuStatus('Starting');

    // Create Waku
    Waku.create().then((waku) => {
      // Once done, put it in the state
      setWaku(waku);
      // And update the status
      setWakuStatus('Started');
    });
  }, [waku, wakuStatus]);

  return (
    <div className="App">
      <header className="App-header">
        // Display the status on the web page
        <p>{wakuStatus}</p>
      </header>
    </div>
  );
}

Connect to Other Peers

The Waku instance needs to connect to other peers to communicate with the network. First, create bootstrapWaku to connect to the Status fleet:

import { getStatusFleetNodes } from 'js-waku';

async function bootstrapWaku(waku) {
  // Retrieve node addresses from https://fleets.status.im/
  const nodes = await getStatusFleetNodes();
  // Connect to the nodes
  await Promise.all(nodes.map((addr) => waku.dial(addr)));
}

Then, bootstrap after Waku is created in the previous useEffect block:

React.useEffect(() => {
  if (!!waku) return;
  if (wakuStatus !== 'None') return;

  setWakuStatus('Starting');

  Waku.create().then((waku) => {
    setWaku(waku);
    setWakuStatus('Connecting');
    bootstrapWaku(waku).then(() => {
      setWakuStatus('Ready');
    });
  });
}, [waku, wakuStatus]);

DappConnect will provide more discovery and bootstrap methods over time, or you can make your own.

Define Message Format

To define the Protobuf message format, use protons

npm install protons

Define SimpleChatMessage with two fields: timestamp and text.

import protons from 'protons';

const proto = protons(`
message SimpleChatMessage {
  uint64 timestamp = 1;
  string text = 2;
}
`);

Send Messages

Create a function that takes the Waku instance and a message to send:

import { WakuMessage } from 'js-waku';

const ContentTopic = `/min-js-web-chat/1/chat/proto`;

async function sendMessage(message, waku, timestamp) {
  const time = timestamp.getTime();

  // Encode to protobuf
  const payload = proto.SimpleChatMessage.encode({
    timestamp: time,
    text: message,
  });

  // Wrap in a Waku Message
  const wakuMessage = await WakuMessage.fromBytes(payload, ContentTopic);
  
  // Send over Waku Relay
  await waku.relay.send(wakuMessage);
}

Then, add a button to send messages to the App function:

function App() {
  const [waku, setWaku] = React.useState(undefined);
  const [wakuStatus, setWakuStatus] = React.useState('None');
  // Using a counter just for the messages to be different
  const [sendCounter, setSendCounter] = React.useState(0);
  
  React.useEffect(() => {
    // ... creates Waku
  }, [waku, wakuStatus]);

  const sendMessageOnClick = () => {
    if (wakuStatus !== 'Ready') return;

    sendMessage(`Here is message #${sendCounter}`, waku, new Date()).then(() =>
      console.log('Message sent')
    );

    setSendCounter(sendCounter + 1);
  };

  return (
    <div className="App">
      <header className="App-header">
        <p>{wakuStatus}</p>
        <button onClick={sendMessageOnClick} disabled={wakuStatus !== 'Ready'}>
          Send Message
        </button>
      </header>
    </div>
  );
}

Receive Messages

To process incoming messages, you need to register an observer on Waku Relay. First, you need to define the function that acts as observer.

As you are passing the function to Waku Relay, it is best to use React.useCallback so that the reference to the function remains the same:

const processIncomingMessage = React.useCallback((wakuMessage) => {
  // Empty message?
  if (!wakuMessage.payload) return;

  // Decode the protobuf payload
  const { timestamp, text } = proto.SimpleChatMessage.decode(
    wakuMessage.payload
  );
  const time = new Date();
  time.setTime(timestamp);

  // For now, just log new messages on the console
  console.log(`message received at ${time.toString()}: ${text}`);
}, []);

Then, add this function as an observer to Waku Relay. Do not forget to delete the observer is the component is being unmounted:

React.useEffect(() => {
  if (!waku) return;

  // Pass the content topic to only process messages related to your dApp
  waku.relay.addObserver(processIncomingMessage, [ContentTopic]);

  // `cleanUp` is called when the component is unmounted, see ReactJS doc.
  return function cleanUp() {
    waku.relay.deleteObserver(processIncomingMessage, [ContentTopic]);
  };
}, [waku, wakuStatus, processIncomingMessage]);

Display Messages

The Waku work is now done. Your dApp is able to send and receive messages using Waku. For the sake of completeness, let's display received messages on the page.

First, modify the App() function to store incoming messages:

function App() {
  //...

  const [messages, setMessages] = React.useState([]);

  const processIncomingMessage = React.useCallback((wakuMessage) => {
    if (!wakuMessage.payload) return;

    const { text, timestamp } = proto.SimpleChatMessage.decode(
      wakuMessage.payload
    );

    const time = new Date();
    time.setTime(timestamp);
    const message = { text, timestamp: time };

    setMessages((currMessages) => {
      return [message].concat(currMessages);
    });
  }, []);
  
  // ...
}

Then, add the messages to the rendering :

function App() {
  // ...

  return (
    <div className="App">
      <header className="App-header">
        <p>{wakuStatus}</p>
        <button onClick={sendMessageOnClick} disabled={wakuStatus !== 'Ready'}>
          Send Message
        </button>
        <ul>
          {messages.map((msg) => {
            return (
              <li>
                <p>
                  {msg.timestamp.toString()}: {msg.text}
                </p>
              </li>
            );
          })}
        </ul>
      </header>
    </div>
  );
}

And Voilà! You should now be able to send and receive messages from two different browsers.

You can see the complete code in the Minimal JS Web Chat App.