07_reactjs_relay.md
1 --- 2 title: Receive and Send Messages Using Waku Relay With ReactJS 3 date: 2021-12-09T14:00:00+01:00 4 weight: 7 5 --- 6 7 # Receive and Send Messages Using Waku Relay With ReactJS 8 9 It is easy to use Waku Connect with ReactJS. 10 In this guide, we will demonstrate how your ReactJS dApp can use Waku Relay to send and receive messages. 11 12 Before starting, you need to choose a _Content Topic_ for your dApp. 13 Check out the [how to choose a content topic guide](/docs/guides/01_choose_content_topic/) to learn more about content topics. 14 For this guide, we are using a single content topic: `/min-react-js-chat/1/chat/proto`. 15 16 # Setup 17 18 Create a new React app: 19 20 ```shell 21 npx create-react-app relay-reactjs-chat 22 cd relay-reactjs-chat 23 ``` 24 25 ## `BigInt` 26 27 Some of js-waku's dependencies use [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) 28 that is [only supported by modern browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#browser_compatibility). 29 30 To ensure that `react-scripts` properly transpile your webapp code, update the `package.json` file: 31 32 ```json 33 { 34 "browserslist": { 35 "production": [ 36 ">0.2%", 37 "not ie <= 99", 38 "not android <= 4.4.4", 39 "not dead", 40 "not op_mini all" 41 ] 42 } 43 } 44 ``` 45 46 ## Setup polyfills 47 48 A number of Web3 dependencies need polyfills. 49 Said polyfills must be explicitly declared when using webpack 5. 50 51 The latest `react-scripts` version uses webpack 5. 52 53 We will describe below a method to configure polyfills when using `create-react-app`/`react-scripts` or webpack 5. 54 This may not be necessary if you do not use `react-scripts` or if you use webpack 4. 55 56 Start by installing the polyfill libraries: 57 58 ```shell 59 npm install --save assert buffer crypto-browserify process stream-browserify 60 ``` 61 62 ### Webpack 5 63 64 If you directly use webpack 5, 65 then you can inspire yourself from this [webpack.config.js](https://github.com/status-im/wakuconnect-vote-poll-sdk/blob/main/examples/mainnet-poll/webpack.config.js). 66 67 ### cra-webpack-rewired 68 69 An alternative is to let `react-scripts` control the webpack 5 config and only override some elements using `cra-webpack-rewired`. 70 71 Install `cra-webpack-rewired`: 72 73 ```shell 74 npm install -D cra-webpack-rewired 75 ``` 76 77 Create a `config/webpack.extend.js` file at the root of your app: 78 79 ```js 80 const webpack = require("webpack"); 81 82 module.exports = { 83 dev: (config) => { 84 // Override webpack 5 config from react-scripts to load polyfills 85 if (!config.resolve) config.resolve = {}; 86 if (!config.resolve.fallback) config.resolve.fallback = {}; 87 Object.assign(config.resolve.fallback, { 88 buffer: require.resolve("buffer"), 89 crypto: require.resolve("crypto-browserify"), 90 stream: require.resolve("stream-browserify"), 91 }); 92 93 if (!config.plugins) config.plugins = []; 94 config.plugins.push( 95 new webpack.DefinePlugin({ 96 "process.env.ENV": JSON.stringify("dev"), 97 }) 98 ); 99 config.plugins.push( 100 new webpack.ProvidePlugin({ 101 process: "process/browser.js", 102 Buffer: ["buffer", "Buffer"], 103 }) 104 ); 105 106 if (!config.ignoreWarnings) config.ignoreWarnings = []; 107 config.ignoreWarnings.push(/Failed to parse source map/); 108 109 return config; 110 }, 111 prod: (config) => { 112 // Override webpack 5 config from react-scripts to load polyfills 113 if (!config.resolve) config.resolve = {}; 114 if (!config.resolve.fallback) config.resolve.fallback = {}; 115 Object.assign(config.resolve.fallback, { 116 buffer: require.resolve("buffer"), 117 crypto: require.resolve("crypto-browserify"), 118 stream: require.resolve("stream-browserify"), 119 }); 120 121 if (!config.plugins) config.plugins = []; 122 config.plugins.push( 123 new webpack.DefinePlugin({ 124 "process.env.ENV": JSON.stringify("prod"), 125 }) 126 ); 127 config.plugins.push( 128 new webpack.ProvidePlugin({ 129 process: "process/browser.js", 130 Buffer: ["buffer", "Buffer"], 131 }) 132 ); 133 134 if (!config.ignoreWarnings) config.ignoreWarnings = []; 135 config.ignoreWarnings.push(/Failed to parse source map/); 136 137 return config; 138 }, 139 }; 140 ``` 141 142 Use `cra-webpack-rewired` in the `package.json`, instead of `react-scripts`: 143 144 ``` 145 "scripts": { 146 - "start": "react-scripts start", 147 - "build": "react-scripts build", 148 - "test": "react-scripts test", 149 - "eject": "react-scripts eject" 150 + "start": "cra-webpack-rewired start", 151 + "build": "cra-webpack-rewired build", 152 + "test": "cra-webpack-rewired test", 153 + "eject": "cra-webpack-rewired eject" 154 }, 155 ``` 156 157 Then, install [js-waku](https://npmjs.com/package/js-waku): 158 159 ```shell 160 npm install --save js-waku 161 ``` 162 163 Start the dev server and open the dApp in your browser: 164 165 ```shell 166 npm run start 167 ``` 168 169 # Create Waku Instance 170 171 In order to interact with the Waku network, you first need a Waku instance. 172 Go to `App.js` and modify the `App` function: 173 174 ```js 175 import { Waku } from "js-waku"; 176 import * as React from "react"; 177 178 function App() { 179 const [waku, setWaku] = React.useState(undefined); 180 const [wakuStatus, setWakuStatus] = React.useState("None"); 181 182 // Start Waku 183 React.useEffect(() => { 184 // If Waku is already assigned, the job is done 185 if (!!waku) return; 186 // If Waku status not None, it means we are already starting Waku 187 if (wakuStatus !== "None") return; 188 189 setWakuStatus("Starting"); 190 191 // Create Waku 192 Waku.create({ bootstrap: { default: true } }).then((waku) => { 193 // Once done, put it in the state 194 setWaku(waku); 195 // And update the status 196 setWakuStatus("Started"); 197 }); 198 }, [waku, wakuStatus]); 199 200 return ( 201 <div className="App"> 202 <header className="App-header"> 203 <p>Waku node's status: {wakuStatus}</p> 204 </header> 205 </div> 206 ); 207 } 208 209 export default App; 210 ``` 211 212 # Wait to be connected 213 214 When using the `bootstrap` option, it may take some time to connect to other peers. 215 To ensure that you have relay peers available to send and receive messages, 216 use the `Waku.waitForRemotePeer()` async function: 217 218 ```js 219 React.useEffect(() => { 220 if (!!waku) return; 221 if (wakuStatus !== "None") return; 222 223 setWakuStatus("Starting"); 224 225 Waku.create({ bootstrap: { default: true } }).then((waku) => { 226 setWaku(waku); 227 setWakuStatus("Connecting"); 228 waku.waitForRemotePeer().then(() => { 229 setWakuStatus("Ready"); 230 }); 231 }); 232 }, [waku, wakuStatus]); 233 ``` 234 235 # Define Message Format 236 237 To define the Protobuf message format, 238 you can use [protobufjs](https://www.npmjs.com/package/protobufjs): 239 240 ```shell 241 npm install protobufjs 242 ``` 243 244 Define `SimpleChatMessage` with two fields: `timestamp` and `text`. 245 246 ```js 247 import protobuf from "protobufjs"; 248 249 const SimpleChatMessage = new protobuf.Type("SimpleChatMessage") 250 .add(new protobuf.Field("timestamp", 1, "uint64")) 251 .add(new protobuf.Field("text", 2, "string")); 252 ``` 253 254 # Send Messages 255 256 Create a function that takes the Waku instance and a message to send: 257 258 ```js 259 import {WakuMessage} from "js-waku"; 260 261 const ContentTopic = `/relay-reactjs-chat/1/chat/proto`; 262 263 function sendMessage(message, waku, timestamp) { 264 const time = timestamp.getTime(); 265 266 // Encode to protobuf 267 const protoMsg = SimpleChatMessage.create({ 268 timestamp: time, 269 text: message, 270 }); 271 const payload = SimpleChatMessage.encode(protoMsg).finish(); 272 273 // Wrap in a Waku Message 274 return WakuMessage.fromBytes(payload, ContentTopic).then((wakuMessage) => 275 // Send over Waku Relay 276 waku.relay.send(wakuMessage) 277 ); 278 } 279 ``` 280 281 Then, add a button to the `App` function: 282 283 ```js 284 function App() { 285 const [waku, setWaku] = React.useState(undefined); 286 const [wakuStatus, setWakuStatus] = React.useState("None"); 287 // Using a counter just for the messages to be different 288 const [sendCounter, setSendCounter] = React.useState(0); 289 290 React.useEffect(() => { 291 // ... creates Waku 292 }, [waku, wakuStatus]); 293 294 const sendMessageOnClick = () => { 295 // Check Waku is started and connected first. 296 if (wakuStatus !== "Ready") return; 297 298 sendMessage(`Here is message #${sendCounter}`, waku, new Date()).then(() => 299 console.log("Message sent") 300 ); 301 302 // For demonstration purposes. 303 setSendCounter(sendCounter + 1); 304 }; 305 306 return ( 307 <div className="App"> 308 <header className="App-header"> 309 <p>{wakuStatus}</p> 310 <button onClick={sendMessageOnClick} disabled={wakuStatus !== "Ready"}> 311 Send Message 312 </button> 313 </header> 314 </div> 315 ); 316 } 317 ``` 318 319 # Receive Messages 320 321 To process incoming messages, you need to register an observer on Waku Relay. 322 First, you need to define the observer function. 323 324 You will need to remove the observer when the component unmount. 325 Hence, you need the reference to the function to remain the same. 326 For that, use `React.useCallback`: 327 328 ```js 329 const processIncomingMessage = React.useCallback((wakuMessage) => { 330 // Empty message? 331 if (!wakuMessage.payload) return; 332 333 // Decode the protobuf payload 334 const {text, timestamp} = SimpleChatMessage.decode(wakuMessage.payload); 335 336 const time = new Date(); 337 time.setTime(timestamp); 338 339 // For now, just log new messages on the console 340 console.log(`message received at ${time.toString()}: ${text}`); 341 }, []); 342 ``` 343 344 Then, add this observer to Waku Relay. 345 Do not forget to delete the observer is the component is being unmounted: 346 347 ```js 348 React.useEffect(() => { 349 if (!waku) return; 350 351 // Pass the content topic to only process messages related to your dApp 352 waku.relay.addObserver(processIncomingMessage, [ContentTopic]); 353 354 // `cleanUp` is called when the component is unmounted, see ReactJS doc. 355 return function cleanUp() { 356 waku.relay.deleteObserver(processIncomingMessage, [ContentTopic]); 357 }; 358 }, [waku, wakuStatus, processIncomingMessage]); 359 ``` 360 361 # Display Messages 362 363 The Waku work is now done. 364 Your dApp is able to send and receive messages using Waku. 365 For the sake of completeness, let's display received messages on the page. 366 367 First, add incoming messages to the state of the `App` component: 368 369 ```js 370 function App() { 371 //... 372 373 const [messages, setMessages] = React.useState([]); 374 375 const processIncomingMessage = React.useCallback((wakuMessage) => { 376 if (!wakuMessage.payload) return; 377 378 const {text, timestamp} = SimpleChatMessage.decode(wakuMessage.payload); 379 380 const time = new Date(); 381 time.setTime(timestamp); 382 const message = {text, timestamp: time}; 383 384 setMessages((messages) => { 385 return [message].concat(messages); 386 }); 387 }, []); 388 389 // ... 390 } 391 ``` 392 393 Then, render the messages: 394 395 ```js 396 function App() { 397 // ... 398 399 return ( 400 <div className="App"> 401 <header className="App-header"> 402 <p>{wakuStatus}</p> 403 <button onClick={sendMessageOnClick} disabled={wakuStatus !== "Ready"}> 404 Send Message 405 </button> 406 <ul> 407 {messages.map((msg) => { 408 return ( 409 <li> 410 <p> 411 {msg.timestamp.toString()}: {msg.text} 412 </p> 413 </li> 414 ); 415 })} 416 </ul> 417 </header> 418 </div> 419 ); 420 } 421 ``` 422 423 And VoilĂ ! You should now be able to send and receive messages. 424 Try out by opening the app from different browsers. 425 426 You can see the complete code in the [Relay ReactJS Chat Example App](https://github.com/status-im/js-waku/tree/main/examples/relay-reactjs-chat).