Creating Responsive Web Apps Using ReactJS, NodeJS and ReactiveX Part 5

in utopian-io •  7 years ago  (edited)

Repository

https://github.com/facebook/react

Welcome to the Tutorial of Creating Responsive Web Apps Using ReactJS, NodeJS and ReactiveX Part 5

What Will I Learn?

  • You will learn how to subscribing to changes in our Sketching Application.
  • You will learn how to deploy those changes into the React Component.
  • You will learn about the issue of slow loading of sketches in the browser.
  • You will learn how to grouping up the events in time segments.
  • You will learn how to fasten the rendering time of our application.
  • You will learn how to test the results and check the changes in browser.

Requirements

System Requirements:
OS Support:
  • Windows 7/8/10
  • macOS
  • Linux

Difficulty

  • Intermediate

Resources

Required Understanding

  • You need a good knowledge of HTML, CSS and JavaScript
  • A fair understanding of Node.js and React.js
  • A thirst for learning and developing something new
  • Prior tutorials that are Part 1,Part 2,Part 3 and Part 4

Tutorial Contents

Description

You have studied about selecting a sketch from the list of sketches and also the handling of sketching events. You have also learned how to publish events over the web sockets and store the sketching events in the RethinkDB in the Fourth Part of the tutorial, if you don't check the fourth tutorial then check the fourth part.
In this Tutorial, you'll learn how to subscribe to sketching changes in your RethinkDB. And then we will see how we can improve the rendering time of our sketching application so that it can load our sketches fast.

Subscribe to Sketching Changes:

This time, let's get the server code done first before you wire up the client code. In the service index file, create a function subscribeToSketchingLines. Make this take an argument object with a socket client, the RethinkDB connection, and the sketchingId.

function subscribeToSketchingLines({ client, connection, sketchingId}) {

}

Now in here, return the result of calling r.table and specifying lines.

return r.table('lines')



Now here is a new RethinkDB query command that you don't know yet called the filter function, which you'll use to query only for the lines associated to the sketching that you're interested in. And to build up a query, use the r variable in here, which is the RethinkDB import. Call a row on it, specify that you want to check the sketching field, and then state that you want it to equal the passed in sketchingId. This just generates a structure that RethinkDB can use to query the table.

.filter(r.row('sketchingId').eq(sketchingId))



So now you want this to be a live query, meaning you want it to bring back new values that match the filter as they get persisted to the db. Add changes onto the query chain. In here, specify that you want the initial values just like you did with the sketching list and timer earlier.

.changes({ include_initial: true})



Now run the query, passing in the connection. Now this whole query chain is actually a promise, so you can chain a then on here, and this should give you access to the cursor.

.run(connection)
  .then((cursor) => {
});

Now use the cursor and call each on it. This expects a callback function with an error parameter and a row parameter. In this function, just use the client socket to emit an event for the line. For the event name, specify a concatenated string of sketchingLine followed by the sketchingId. Now you do this because the event now has a type of category. It only makes sense for this sketching. You don't want to mistakenly send events, meaning lines, from one sketching to another, so it makes sense for the actual event topic, the string, to have the sketchingId embedded on it.

.then((cursor) => {
    cursor.each((err, lineRow) => client.emit(`sketchingLine:${sketchingId}`, lineRow.new_val));
  });

As the payload of this event, just send through thenew_val of the row as you did earlier with sketchings too. Now hook it up on the socket layer next to where you handle all the other socket interaction. Below publishLine, add a client.on call and pass subscribeToSketchingLines as the event name. This is what's going to be sent from the client to show its interest in a particular sketching's lines. Provide it with a callback function, which should get the sketchingId from the client side.

client.on('subscribeToSketchingLines', (sketchingId) => {
      subscribeToSketchingLines({
        client,
        connection,
        sketchingId,
      });
    });

And in here, just call the function that you just created subscribeToSketchingLines, passing in an argument object with a client, the connection, and sketchingId. So that's it for the server side of providing lines to clients over the WebSockets.

Now after the updates Index.js file of the server looks like this;

//index.js//
const r = require('rethinkdb');
const io = require('socket.io')();

function createSketching({ connection, name }) {
  return r.table('sketchings')
  .insert({
    name,
    timestamp: new Date(),
  })
  .run(connection)
  .then(() => console.log('created a new sketching with name ', name));
}

function subscribeToSketchings({ client, connection }) {
  r.table('sketchings')
  .changes({ include_initial: true })
  .run(connection)
  .then((cursor) => {
    cursor.each((err, sketchingRow) => client.emit('sketching', sketchingRow.new_val));
  });
}

function handleLinePublish({ connection, line }) {
  console.log('saving line to the db')
  r.table('lines')
  .insert(Object.assign(line, { timestamp: new Date() }))
  .run(connection);
}

function subscribeToSketchingLines({ client, connection, sketchingId}) {
  return r.table('lines')
  .filter(r.row('sketchingId').eq(sketchingId))
  .changes({ include_initial: true, include_types: true })
  .run(connection)
  .then((cursor) => {
    cursor.each((err, lineRow) => client.emit(`sketchingLine:${sketchingId}`, lineRow.new_val));
  });
}

r.connect({
  host: 'localhost',
  port: 28015,
  db: 'awesome_whiteboard'
}).then((connection) => {
  io.on('connection', (client) => {
    client.on('createSketching', ({ name }) => {
      createSketching({ connection, name });
    });

    client.on('subscribeToSketchings', () => subscribeToSketchings({
      client,
      connection,
    }));

    client.on('publishLine', (line) => handleLinePublish({
      line,
      connection,
    }));

    client.on('subscribeToSketchingLines', (sketchingId) => {
      subscribeToSketchingLines({
        client,
        connection,
        sketchingId,
      });
    });
  });
});


const port = 8000;
io.listen(port);
console.log('listening on port ', port);

Deploying Changes to React Component:

Now let's go and wire up the clients to get these lines onto the React Sketching component through its props to get it to render. In the api file in the client source folder, create a new function called subscribeToSketchingLines, and make it take a sketchingId and a callback function.

function subscribeToSketchingLines(sketchingId, cb) {
  );

First, subscribe to the values coming through from the server by calling on on the socket, specifying the event name that's a concatenation between sketchingLine and the sketchingId. Remember, this is to make it more specific to ensure that you don't cross contaminate events when a user switches between sketchings and as the event handler, just pass it on to the callback.

socket.on(‘sketchingLine:${sketchingId}’, cb),



Now that you know the event will be handled, you can emit the event that will tell the server that you're interested in getting sketching lines, so call emit on the socket and pass throughsubscribeToSketchingLines and pass through the sketchingId.

socket.emit('subscribeToSketchingLines', sketchingId);



Say you were subscribing to events and also telling the server that it should open up a query for us and start piping through the events by emitting an event from the client.

subscribeToSketchingLines,



At the bottom, export this function from this file. Now to use this functionality, go to the Sketching component in Sketching.js. Import this new subscribeToSketchingLines function.

import { publishLine, subscribeToSketchingLines } from './api';



You're going to store the lines coming back from the server on state, so add a default state variable on this component with an empty array called lines.

state = {
    lines: [],
  }

Add a componentDidMount method because you'll be subscribing to the data in here. This gets called by React when your component has been wired up, so it's a good place to start subscribing to data from the server. Call the subscribeToSketchingLines function that you imported and pass on the ID of the sketching that you get through on this component's props. Also, pass a callback function with a line parameter

componentDidMount() {
    subscribeToSketchingLines(this.props.sketching.id, (line) => {
  });
  }

In this callback, call setState using the function syntax this time because you want to ensure that you don't work with stale state. You don't want to go and append a new line onto the array as it was a few updates ago. This expects a function that it can call with the previous state being passed in.

this.setState((prevState) => {
        return {
          lines: [...prevState.lines, line],
        };
      });

In here you can just say that you want the new state to have the lines array with the new line added onto it. Good! So you're subscribing to values coming over the WebSocket and adding them to the array on state as you receive them using the functional setState syntax.

Now you can go and use the lines that you have on state. Down in the window where you have the Canvas component, go add a lines prop on it and set that to the lines value that you have on state, and that's it.

lines={this.state.lines}



Now go and open two browsers side by side. When you draw something on the one, you should see it coming through on the other one in real time.


Pretty cool! So we have done it.

Delay in Loading Sketch:

So the call logic is done, and you can get a sketching to update in close to real time between two different browsers. That's cool, but you might've noticed a problem as you played around with it in your browser. When you have a sketching with a bit more detail, meaning it'll have more lines in the database, it takes a very long time for it to show the sketching when you open it.


Take this for example. I've already clicked on it, and it takes way too long to actually show the sketching that we've selected. Not what I would call real time. This takes long because React will try to render the Sketching component each time that it receives a new line to be drawn. This particular sketching contains 6,400 lines,

So it tries to render the Sketching component 6,400 times. Well, actually 6,401 because it renders the first time on default state when there are no lines present. You could override the Sketching component's shouldComponentUpdate in order to avoid reconciliation. You could use this approach to ensure that you wait for a while of inactivity without receiving any new line events before rendering. But why should your component be aware of the architecture that it lives in and cater for this? It would be nice if we could keep the Sketching component simple and handle the issue inside of the API where we're aware that we're working with WebSockets.

And the way you can do this is by batching up the updates a bit and sending through chunked updates to your component so that it can render those. So basically instead of sending through each line to the canvas props, you're going to send it through in buffers, giving it time to go apply a buffer of lines together. You're going to do the bulk of this work in the api.js file and try to keep your component as simple as possible. This is also where you'll first start using RxJS in this project. RxJS provides you with functions and abstractions that help you deal with streaming. Because your event coming through from the server is definitely a stream, it makes sense to use it. You could definitely code up something yourself without relying on RxJS, but it would be complicated and difficult to follow.

Why reinvent the wheel?

RxJS is part of the package.json file, so you've already got it installed, and you can just go ahead and use it. In the api.js file, import Rx from rxjs/Rx.

import Rx from 'rxjs/Rx';



Now what you want to do is change the subscribeToSketchingLines function so that it has the same signature, but instead of calling back on the callback function for each event, you want to batch it up and call back on it with an array of lines. But first you need to get an Rx observable representing your data, also known as a stream. This will represent the events coming through from the WebSocket server. Luckily, Rx has an easy way of doing this. Create a new const called lineStream.

  const lineStream = Rx.Observable.fromEventPattern(
  );

Now set this to the result of calling Rx.Observable.fromEventPattern, which is a factory function that'll return the observable that'll send through an item every time the event fires. This function takes two functions as parameters. The first is for you to subscribe to the event and make the RxJS observable the handler. Rx will pass this function to the handler that you need to hook up to your event. Let's call that h, short for handler.

h => socket.on('sketchingLine:${sketchingId}', h),



Now as you did earlier, just use socket.on to subscribe to the event, but instead of choosing the callback from the function from up here, you wire up the handler that RxJS passes you, which you called h. So now every time an event gets published over the socket channel, it'll send it through on this observable, allowing you to use all kinds of cool stream operators on it. You'll see soon enough how powerful this is. The next function that Rx will pass you allows you to do the reverse, to basically unsubscribe from the event.

h => socket.off('sketchingLine:${sketchingId}', h),



This is so that you get a chance to unsubscribe from events when the stream wants to be disposed. So again, make it take the handler, and this time call socket.off, using the same event name, and pass in the handler. Now the observable can easily get disposed and garbage collected.

Grouping the Events In Time Sagments:

Great! So now you have an observable that you can use to buffer it up. What you want to do is buffer up the events being published up from the server based on time. If, in the first 100 mm since starting, 2 events came through, then the observable should return those 2 events on the callback. If, in the next 100 mm, 50 events came through, then 50 events should be returned on the callback. If, in the next 100 mm, no events came through, it should not call the callback. So you're basically just grouping it up in time segments.


To do that in code, create a const called bufferedTimeStream and set that to the result of calling a method on lineStream called bufferTime. This is what RxJS calls an operator. It's a method that you execute on the observable that allows you to consume the stream of data coming through in it in a certain way.

const bufferedTimeStream = lineStream
  .bufferTime(100)

Interestingly enough, it'll also return an observable, but the values in the observable will now come through according to the way that bufferTime decided. BufferTime takes a numeric value, which is the number of milliseconds that it should use to buffer items up, which, in our case, is 100. What you want to do now is map over this and ensure that you have an object containing the actual lines on it.

.map(lines => ({ lines }));



This isn't really necessary, but I wanted to show you that it's possible to do something like this to get access to the array with the values in it from the buffer and do something else with it. Now, how do you feed the values from this observable over the callback to the subscribeToSketchings function? Easy. Just call a method on the bufferedTimeStream called subscribe, and then pass the callback function in there. It'll call the callback function as soon as an item comes through.

bufferedTimeStream.subscribe(linesEvent => cb(linesEvent));



Now the api.js file after the updates looks like this;

//api.js//
import openSocket from 'socket.io-client';
import Rx from 'rxjs/Rx';
const socket = openSocket('http://localhost:8000');

function subscribeToSketchings(cb) {
  socket.on('sketching', sketching => cb(sketching));
  socket.emit('subscribeToSketchings');
}

function createSketching(name) {
  socket.emit('createSketching', { name });
}

function publishLine({ sketchingId, line }) {
  socket.emit('publishLine', { sketchingId, ...line });
}

function subscribeToSketchingLines(sketchingId, cb) {

  const lineStream = Rx.Observable.fromEventPattern(
    h => socket.on(`sketchingLine:${sketchingId}`, h),
    h => socket.off(`sketchingLine:${sketchingId}`, h),
  );

  const bufferedTimeStream = lineStream
  .bufferTime(100)
  .map(lines => ({ lines }));

  bufferedTimeStream.subscribe(linesEvent => cb(linesEvent));
  socket.emit('subscribeToSketchingLines', sketchingId);
}

export {
  publishLine,
  createSketching,
  subscribeToSketchings,
  subscribeToSketchingLines,
};

Now that this function returns the line items in an array on an object, you need to change the Sketching component to expect that. Head on over to the Sketching.js file. In the callback function that you're passing to the subscribe function, change the parameter name to linesEvent in order to communicate better what it does.

subscribeToSketchingLines(this.props.sketching.id, (linesEvent) => {
});

And where you're currently just appending the line that you got, change this to add the lines in the array on the event to the array on state by using the spread syntax.

lines: [...prevState.lines, ...linesEvent.lines],



Now the Sketching.js file after the updates looks like this;

//Sketching.js//
import React, { Component } from 'react';
import Canvas from 'simple-react-canvas';
import { publishLine, subscribeToSketchingLines } from './api';

class Sketching extends Component {
  state = {
    lines: [],
  }

  componentDidMount() {
    subscribeToSketchingLines(this.props.sketching.id, (linesEvent) => {
      this.setState((prevState) => {
        return {
          lines: [...prevState.lines, ...linesEvent.lines],
        };
      });
    });
  }

  handleDraw = (line) => {
    publishLine({
      sketchingId: this.props.sketching.id,
      line,
    });
  }

  render() {
    return (this.props.sketching) ? (
      <div
        className="Sketching"
      >
        <div className="Sketching-title">{this.props.sketching.name}</div>
        <Canvas
          onDraw={this.handleDraw}
          sketchingEnabled={true}
          lines={this.state.lines}
        />
      </div>
    ) : null;
  }
}
export default Sketching;

Checking the Results:

So go and check out the results in the browser. I'm opening the same sketching that took a very long time to open earlier, and it renders the sketching immediately.


The buffering really made a difference, and the best is our Sketching component remained really simple.

Summary:

Great! So you've now got a working collaborative real-time whiteboard. The best thing is you can even run the WebSockets on a separate service because using RethinkDB allows you to scale out through the use of its live query functionality. You can imagine how useful this stack could be when you need to build out something real time. That said, to get this sketching app that you build to scale well, it'll also need to be robust. It should handle it seamlessly when the server that the user is connected to goes down. It should also handle the scenario where the user loses her connection to the server because a lot of apps are used on phones with spotty network connections. In the next tutorial, that's exactly what you'll learn, how to handle failure scenarios in a real-time, socket-based stack, like the one that you've been working on.

Curriculum

Project Repository

Thank you

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for your contribution.

  • Good work, keep it up!

Looking forward to your upcoming tutorials.

Your contribution has been evaluated according to Utopian rules and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post,Click here


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

Thank You Sir...Your suggestions and sayings really help me.Sir this is the fifth part of the series please check this in the questionaire.

Hey @engr-muneeb, your contribution was unvoted because we found out that it did not follow the Utopian rules.

Upvote this comment to help Utopian grow its power and help other Open Source contributions like this one.

Want to chat? Join us on Discord.