From 10be3e4e7f043dc232884dfb04e68068d1179004 Mon Sep 17 00:00:00 2001 From: Franck R Date: Mon, 10 Jan 2022 12:11:15 +1100 Subject: [PATCH] Upgrade React guides for react-scripts 5.0.0 (#19) --- content/docs/guides/07_reactjs_relay.md | 158 ++++++++++-- content/docs/guides/08_reactjs_store.md | 319 +++++++++++++++++++++--- 2 files changed, 432 insertions(+), 45 deletions(-) diff --git a/content/docs/guides/07_reactjs_relay.md b/content/docs/guides/07_reactjs_relay.md index 023258a..66bfe52 100644 --- a/content/docs/guides/07_reactjs_relay.md +++ b/content/docs/guides/07_reactjs_relay.md @@ -17,8 +17,140 @@ For this guide, we are using a single content topic: `/min-react-js-chat/1/chat/ Create a new React app: ```shell -npx create-react-app min-react-js-chat -cd min-react-js-chat +npx create-react-app relay-reactjs-chat +cd relay-reactjs-chat +``` + +## `BigInt` + +Some of js-waku's dependencies use [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) +that is [only supported by modern browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#browser_compatibility). + +To ensure that `react-scripts` properly transpile your webapp code, update the `package.json` file: + +```json +{ + "browserslist": { + "production": [ + ">0.2%", + "not ie <= 99", + "not android <= 4.4.4", + "not dead", + "not op_mini all" + ] + } +} +``` + +## Setup polyfills + +A number of Web3 dependencies need polyfills. +Said polyfills must be explicitly declared when using webpack 5. + +The latest `react-scripts` version uses webpack 5. + +We will describe below a method to configure polyfills when using `create-react-app`/`react-scripts` or webpack 5. +This may not be necessary if you do not use `react-scripts` or if you use webpack 4. + +Start by installing the polyfill libraries: + +```shell +npm install assert buffer crypto-browserify stream-browserify +``` + +### Webpack 5 + +If you directly use webpack 5, +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). + +### cra-webpack-rewired + +An alternative is to let `react-scripts` control the webpack 5 config and only override some elements using `cra-webpack-rewired`. + +Install `cra-webpack-rewired`: + +```shell +npm install -D cra-webpack-rewired +``` + +Create a `config/webpack.extend.js` file at the root of your app: + +```js +const webpack = require('webpack'); + +module.exports = { + dev: (config) => { + // Override webpack 5 config from react-scripts to load polyfills + if (!config.resolve) config.resolve = {}; + if (!config.resolve.fallback) config.resolve.fallback = {}; + Object.assign(config.resolve.fallback, { + buffer: require.resolve('buffer'), + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }); + + if (!config.plugins) config.plugins = []; + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.ENV': JSON.stringify('dev'), + }) + ); + config.plugins.push( + new webpack.ProvidePlugin({ + process: 'process/browser.js', + Buffer: ['buffer', 'Buffer'], + }) + ); + + if (!config.ignoreWarnings) config.ignoreWarnings = []; + config.ignoreWarnings.push(/Failed to parse source map/); + + return config; + }, + prod: (config) => { + // Override webpack 5 config from react-scripts to load polyfills + if (!config.resolve) config.resolve = {}; + if (!config.resolve.fallback) config.resolve.fallback = {}; + Object.assign(config.resolve.fallback, { + buffer: require.resolve('buffer'), + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }); + + if (!config.plugins) config.plugins = []; + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.ENV': JSON.stringify('prod'), + }) + ); + config.plugins.push( + new webpack.ProvidePlugin({ + process: 'process/browser.js', + Buffer: ['buffer', 'Buffer'], + }) + ); + + if (!config.ignoreWarnings) config.ignoreWarnings = []; + config.ignoreWarnings.push(/Failed to parse source map/); + + return config; + }, +}; +``` + +Use `cra-webpack-rewired` in the `package.json`, instead of `react-scripts`: + +``` + "scripts": { +- "start": "react-scripts start", +- "build": "react-scripts build", +- "test": "react-scripts test", +- "eject": "react-scripts eject" ++ "start": "cra-webpack-rewired start", ++ "build": "cra-webpack-rewired build", ++ "test": "cra-webpack-rewired test", ++ "eject": "cra-webpack-rewired eject" + }, ``` Then, install [js-waku](https://npmjs.com/package/js-waku): @@ -33,15 +165,6 @@ Start the dev server and open the dApp in your browser: npm run start ``` -Note: We have noticed some [issues](https://github.com/status-im/js-waku/issues/165) with React bundling due to `npm` pulling an old version of babel. -If you are getting an error about the [optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) -character not being valid, try cleaning up and re-installing your dependencies: - -```shell -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. @@ -76,12 +199,13 @@ function App() { return (
- // Display the status on the web page -

{wakuStatus}

+

Waku node's status: {wakuStatus}

); } + +export default App; ``` # Wait to be connected @@ -136,9 +260,9 @@ Create a function that takes the Waku instance and a message to send: ```js import { WakuMessage } from 'js-waku'; -const ContentTopic = `/min-react-js-chat/1/chat/proto`; +const ContentTopic = `/relay-reactjs-chat/1/chat/proto`; -function sendMessage(message, timestamp, waku) { +function sendMessage(message, waku, timestamp) { const time = timestamp.getTime(); // Encode to protobuf @@ -184,7 +308,7 @@ function App() {

{wakuStatus}

-
@@ -302,4 +426,4 @@ function App() { And VoilĂ ! You should now be able to send and receive messages. Try out by opening the app from different browsers. -You can see the complete code in the [Minimal ReactJS Chat App](https://github.com/status-im/js-waku/tree/main/examples/min-react-js-chat). +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). diff --git a/content/docs/guides/08_reactjs_store.md b/content/docs/guides/08_reactjs_store.md index 725e63a..e0c0159 100644 --- a/content/docs/guides/08_reactjs_store.md +++ b/content/docs/guides/08_reactjs_store.md @@ -33,8 +33,140 @@ Check out the [how to choose a content topic guide](/docs/guides/01_choose_conte Create a new React app: ```shell -npx create-react-app my-app -cd my-app +npx create-react-app store-reactjs-chat +cd store-reactjs-chat +``` + +## `BigInt` + +Some of js-waku's dependencies use [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) +that is [only supported by modern browsers](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#browser_compatibility). + +To ensure that `react-scripts` properly transpile your webapp code, update the `package.json` file: + +```json +{ + "browserslist": { + "production": [ + ">0.2%", + "not ie <= 99", + "not android <= 4.4.4", + "not dead", + "not op_mini all" + ] + } +} +``` + +## Setup polyfills + +A number of Web3 dependencies need polyfills. +Said polyfills must be explicitly declared when using webpack 5. + +The latest `react-scripts` version uses webpack 5. + +We will describe below a method to configure polyfills when using `create-react-app`/`react-scripts` or webpack 5. +This may not be necessary if you do not use `react-scripts` or if you use webpack 4. + +Start by installing the polyfill libraries: + +```shell +npm install assert buffer crypto-browserify stream-browserify +``` + +### Webpack 5 + +If you directly use webpack 5, +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). + +### cra-webpack-rewired + +An alternative is to let `react-scripts` control the webpack 5 config and only override some elements using `cra-webpack-rewired`. + +Install `cra-webpack-rewired`: + +```shell +npm install -D cra-webpack-rewired +``` + +Create a `config/webpack.extend.js` file at the root of your app: + +```js +const webpack = require('webpack'); + +module.exports = { + dev: (config) => { + // Override webpack 5 config from react-scripts to load polyfills + if (!config.resolve) config.resolve = {}; + if (!config.resolve.fallback) config.resolve.fallback = {}; + Object.assign(config.resolve.fallback, { + buffer: require.resolve('buffer'), + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }); + + if (!config.plugins) config.plugins = []; + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.ENV': JSON.stringify('dev'), + }) + ); + config.plugins.push( + new webpack.ProvidePlugin({ + process: 'process/browser.js', + Buffer: ['buffer', 'Buffer'], + }) + ); + + if (!config.ignoreWarnings) config.ignoreWarnings = []; + config.ignoreWarnings.push(/Failed to parse source map/); + + return config; + }, + prod: (config) => { + // Override webpack 5 config from react-scripts to load polyfills + if (!config.resolve) config.resolve = {}; + if (!config.resolve.fallback) config.resolve.fallback = {}; + Object.assign(config.resolve.fallback, { + buffer: require.resolve('buffer'), + crypto: require.resolve('crypto-browserify'), + stream: require.resolve('stream-browserify'), + }); + + if (!config.plugins) config.plugins = []; + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.ENV': JSON.stringify('prod'), + }) + ); + config.plugins.push( + new webpack.ProvidePlugin({ + process: 'process/browser.js', + Buffer: ['buffer', 'Buffer'], + }) + ); + + if (!config.ignoreWarnings) config.ignoreWarnings = []; + config.ignoreWarnings.push(/Failed to parse source map/); + + return config; + }, +}; +``` + +Use `cra-webpack-rewired` in the `package.json`, instead of `react-scripts`: + +``` + "scripts": { +- "start": "react-scripts start", +- "build": "react-scripts build", +- "test": "react-scripts test", +- "eject": "react-scripts eject" ++ "start": "cra-webpack-rewired start", ++ "build": "cra-webpack-rewired build", ++ "test": "cra-webpack-rewired test", ++ "eject": "cra-webpack-rewired eject" + }, ``` Then, install [js-waku](https://npmjs.com/package/js-waku): @@ -49,7 +181,8 @@ Start the dev server and open the dApp in your browser: npm run start ``` -Note: We have noticed some [issues](https://github.com/status-im/js-waku/issues/165) with React bundling due to `npm` pulling an old version of babel. +{{< hint info >}} +We have noticed some [issues](https://github.com/status-im/js-waku/issues/165) with React bundling due to `npm` pulling an old version of babel. If you are getting an error about the [optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) character not being valid, try cleaning up and re-installing your dependencies: @@ -57,6 +190,7 @@ character not being valid, try cleaning up and re-installing your dependencies: rm -rf node_modules package-lock.json npm install ``` +{{< /hint >}} # Create Waku Instance @@ -90,12 +224,13 @@ function App() { return (
- // Display the status on the web page

{wakuStatus}

); } + +export default App; ``` # Wait to be connected @@ -217,40 +352,43 @@ const processMessages = (retrievedMessages) => { }; ``` -Finally, pass `processMessage` in `WakuStore.queryHistory` as the `callback` value: +Pass `processMessage` in `WakuStore.queryHistory` as the `callback` value: ```js waku.store .queryHistory([ContentTopic], { callback: processMessages }); ``` -All together, you should now have: +Finally, create a `Messages` component to render the messages: -```js -const ContentTopic = '/toy-chat/2/huilong/proto'; +```tsx +function Messages(props) { + return props.messages.map(({ text, timestamp, nick }) => { + return ( +
  • + ({formatDate(timestamp)}) {nick}: {text} +
  • + ); + }); +} +function formatDate(timestamp) { + return timestamp.toLocaleString([], { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); +} +``` + +Use `Messages` in the `App` function: + +```tsx function App() { // [..] - // Store messages in the state - const [messages, setMessages] = React.useState([]); - - React.useEffect(() => { - if (wakuStatus !== 'Connected') return; - - const processMessages = (retrievedMessages) => { - const messages = retrievedMessages.map(decodeMessage).filter(Boolean); - - setMessages((currentMessages) => { - return currentMessages.concat(messages.reverse()); - }); - }; - - waku.store - .queryHistory([ContentTopic], { callback: processMessages }) - .catch((e) => { - console.log('Failed to retrieve messages', e); - }); - }, [waku, wakuStatus]); return (
    @@ -264,13 +402,138 @@ function App() {
    ); } +``` +All together, you should now have: + +```js +import { Waku } from 'js-waku'; +import * as React from 'react'; +import protons from 'protons'; + +const ContentTopic = '/toy-chat/2/huilong/proto'; + +const proto = protons(` +message ChatMessage { + uint64 timestamp = 1; + string nick = 2; + bytes text = 3; +} +`); + +function App() { + const [waku, setWaku] = React.useState(undefined); + const [wakuStatus, setWakuStatus] = React.useState('None'); + const [messages, setMessages] = React.useState([]); + + // Start Waku + React.useEffect(() => { + // If Waku status not None, it means we are already starting Waku + if (wakuStatus !== 'None') return; + + setWakuStatus('Starting'); + + // Create Waku + Waku.create({ bootstrap: true }).then((waku) => { + // Once done, put it in the state + setWaku(waku); + // And update the status + setWakuStatus('Connecting'); + }); + }, [waku, wakuStatus]); + + + React.useEffect(() => { + if (!waku) return; + + if (wakuStatus === 'Connected') return; + + waku.waitForConnectedPeer().then(() => { + setWakuStatus('Connected'); + }); + }, [waku, wakuStatus]); + + React.useEffect(() => { + if (wakuStatus !== 'Connected') return; + + const processMessages = (retrievedMessages) => { + const messages = retrievedMessages.map(decodeMessage).filter(Boolean); + + setMessages((currentMessages) => { + return currentMessages.concat(messages.reverse()); + }); + }; + + waku.store + .queryHistory([ContentTopic], { callback: processMessages }) + .catch((e) => { + console.log('Failed to retrieve messages', e); + }); + }, [waku, wakuStatus]); + + return ( +
    +
    +

    {wakuStatus}

    +

    Messages

    +
      + +
    +
    +
    + ); +} + +export default App; + +function decodeMessage(wakuMessage) { + if (!wakuMessage.payload) return; + + const { timestamp, nick, text } = proto.ChatMessage.decode( + wakuMessage.payload + ); + + // All fields in protobuf are optional so be sure to check + if (!timestamp || !text || !nick) return; + + const time = new Date(); + time.setTime(timestamp); + + const utf8Text = Buffer.from(text).toString('utf-8'); + + return { text: utf8Text, timestamp: time, nick }; +} + +function Messages(props) { + return props.messages.map(({ text, timestamp, nick }) => { + return ( +
  • + ({formatDate(timestamp)}) {nick}: {text} +
  • + ); + }); +} + +function formatDate(timestamp) { + return timestamp.toLocaleString([], { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); +} ``` Note that `WakuStore.queryHistory` select an available store node for you. However, it can only select a connected node, which is why the bootstrapping is necessary. It will throw an error if no store node is available. +If no message are returned, +then you can use https://status-im.github.io/js-waku/ to send some messages on the +toy chat topic and refresh your app. + ## Filter messages by send time By default, Waku Store nodes store messages for 30 days.