651.288.7000 info@intertech.com

Setting Up The Front End As A “New Web” Application

In Part 2 of this series, we discussed setting up our server.  Now, we’re going to discuss setting up our front end as a web app.  There are several frameworks available to get us going.  While my favorite framework is Google’s Angular framework, we’re going to use Facebook’s React framework.

In my opinion, Angular follows a little more conventional flow in terms of application state and data binding.  There is no need to tell Angular you’ve changed your data and update front-end components, because it’s binding mechanics is able to figure out when these things happen.  Angular 2+ is written in TypeScript so using it as the only real option for development (opposed to JavaScript).

In contract, React has much less automation in binding.  Further, as a rule, your data in React may not be mutable.  When you make a change your data, you must copy your application state, and make the change on your new instance of the data.  This may sound scary, but it’s not as bad as you might think.  Redux is a 3rd party package commonly used with React, which makes this a bit easier.

Despite my preferences, our example is going to use React.  Because our sample app is very simple, React is a little easier to work with.

More On Front End Frameworks & React

If you’re familiar with ASP.NET or PHP, then the concept of front-end frameworks like React and Angular shouldn’t be a stretch to understand.  Where ASP.NET and PHP provide a way of creating “dynamic” web content from the server, React and Angular do a similar function but on the client side (browser).  The huge advantage here is that all of the code is run in the browser, removing the need to post-back and reload your pages, because all content is in a single page.  In fact, this is where the term “single page application” or “SPA” comes from.

React and Angular have the same basic goals, but work much differently and have their own infrastructures.  If you’re familiar with one framework, it takes a little work to transition to the other.  In my opinion, it’s best to start with Angular and move to React, because React has special constraints.  Transitioning the other way tends to make one think Angular is bound by the same constraints, which is not the case.

You can learn more about each framework from their websites, which I’ve included below:
https://angular.io/
https://reactjs.org/

Clone From GitHub

You can find my GitHub repository for this example at the link below.
https://github.com/rolson-intertech/chat-app-client

If you clone the github repo, just be sure to execute the following command at the command line to fully set it up after cloning.

npm install

Environment

Good news!  You’ve already setup your environment in Part 2, so you have nothing to do here.  This is one of my favorite aspects of an application like this, because of the seemlessness between front-end and back-end development.

React Project Setup

Our first goal is to kickoff our React project using React’s setup script (named create-react-app) with the TypeScript template.  Here, we’re going to use NPM under the covers, but it’s done a little differently.  Because create-react-app is one of those commands we only use once in a while, and it may change often, it would mean that we ‘d have to reinstall it every time we created a new React application.  NPX is a command that comes with NPM, and lets us run a command from an NPM package within NPM’s repository, without installing it.  This means that every time we use it with the create-react-app command, it will always use the most up-to-date version!  To learn more about NPX, follow this link: NPX Blog Post.

To setup our front-end project, navigate to the root folder of our project in your command line (same folder the server folder is in).  Then execute the following command:

npx create-react-app client –template typescript

This does all of the dirty work for us!  It will create a new folder called “client”, and install all of the base React code for a starter project.  If you examine the folder, you’ll notice that we have a tsconfig.json file and package.json file, just like our server project.  Our project is managed by NPM and our tools use Node to run, but always remember that these are only required for development.  The final output is plain old JavaScript and HTML that runs in your browser.

Files & Folder Structure

If everything went as expected, you should have the folder structure shown on the right.

The src folder is where all of your source files go.  This will include CSS files for your React components, as well as the actual component files and any other source code.

Feel free to organize your code as you wish.  I tend to place my components in their own folders, and have a single root folder for all of my non-UI source code (like interaction with the server, etc).

Of course, if your files are moved around, any references to those files (i.e. through import or require statements) must be updated to reflect the new locations.  Most IDEs will handle this for you, but it’s important to be aware of.

The public folder holds files that do not commonly change, and will be added to the final output.

Webpack & index.html

You might recall my mention of webpack in Part 1 of this series.  In case you forgot, webpack is used under the hood to “compile” all of our front-end code into a single JavaScript output.

Webpack treats index.html a little special.  As you might expect, index.html is the root HTML file that loads in the user’s browser.  What’s not obvious is that webpack will copy this file to the output folder, and add references to our compiled JavaScript code for us.

It’s perfectly acceptable to add javascript references in this file, when needed, but that should rarely need to happen.  Most references are added through references in our TypeScript code, though some third party packages, like FontAwesome, may have your add other references.

3rd Party Packages & Installs

We are going to use a few 3rd party libraries in our application.  Some are required, like Socket.IO, and others just make things smoother for us like PrimeFaces’ React Components (primereact).  These are installed just like any other library, using the “npm install –save” command.  I’ll go through each in order below, but you may use the cheat sheet to the right if you want to skip the explanations.

socket.io-client

As you might expect, we need the client implementation of Socket.IO to facilitate the front-end functionality for us.  Because type definitions aren’t bundled with the library, we’ll need to install those to.  Use the following command to add both references to our project:

npm install –save socket.io-client @types/socket.io-client

ReactiveX for JavaScript (rxjs)

ReactiveX should not be confused with React, as they are completely different things.

ReactiveX can be used in different ways, but for our purposes, we’re just going to be using it as an event system.  I’m going to oversimplify these concepts, but you can read more about RxJs and ReactiveX at the following two links respectively.

https://rxjs-dev.firebaseapp.com/guide/overview 
http://reactivex.io/

The two basic classes we’re going to use in ReactiveX are Subjects and Observables.

Observables can be thought of as an abstract event source without a clear way of firing the event.  Subjects are Observables which we have access to the event trigger.  Essentially, we limit access to our Subjects for private use in a class, and make an Observable available to consumers, so they can respond to events fired from our Subject.

When we subscribe to RxJs events (Observables), we always get a Subscription object back.  We can use this Subscription to unsubscribe from events when we’re done with them.  For instance, when we leave a React page and it is destroyed, we want to be sure to unsubscribe any of our event handlers.  Not doing so may cause your application to degrade in performance over time, since event handlers will continue to be called for objects that no longer exist in our application.

Again, this is an oversimplification, but the explanations here are appropriate for how they’re used in this tutorial.

Prime Faces React Components (primereact)

Prime Faces’ control library gives us a number of fully functional controls out of the box.  Front-end frameworks make it easier for us to create our own controls, but this is free and let’s us focus more on our application logic than our controls.  primereact requires us to install a few libraries, which we’ll do in one command:

npm install –save primereact primeicons classnames react-transition-group

Adding Theme References:

After doing this, we need to add our theme references to our index.tsx file, located in the srcfolder.  Because webpack bundles all of our files for us, this adds the CSS files to our JavaScript output, and we won’t have to reference these files through the index.html file.  More importantly, it’s a few less files to remember to add to the output.

I will show the complete file later, but add the following lines below the existing “import” statements to the src/index.tsx file. 

import 'primereact/resources/themes/nova-light/theme.css';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';

You can find a list of the controls and how to use them at the following link: https://www.primefaces.org/primereact/showcase/#/setup

Installation TLDR;

At the command line, execute the following commands:

npm install –save socket.io-client @types/socket.io-client  rxjs primereact primeicons classnames react-transition-group node-sass rxjs

npm install 

Font Awesome

FontAwesome is a great place to get free icons, which work through CSS styles.  You don’t even have to download anything!  I use them in almost all of my projects, but I’m not including them here because it requires you to sign up and is more than we absolutely need for this example.  On the other hand, this is a fantastic resource, and I strongly recommend using them when you’re in need of icons.

Link: https://fontawesome.com/

 

React File Types (JSX & TSX)

While it’s possible to create our react content 100% in TypeScript/JavaScript, it’s difficult to imagine the markup in that way.

React has an added syntax that we use in our script that allows us to write more HTML-like markup, called JSX.

Files with the extensions JSX or TSX are expected to have JSX markup content, and is will be compiled that way.  Other than the additional markup, these are 100%  JavaScript or TypeScript files (JS and TS extensions respectively).

 

Promises

In a nutshell, a promise is a convenient way of working with asynchronous code without using callback functions.

Promises are used in various places of this project and the server project, so having an idea of what they do is important.

If you’re not familiar with promises, check out my blog post on the topic.  You can find it here.

SASS (node-sass)

There’s nothing wrong with using vanilla css styling in React. I personally like using SASS however.

All we need to do is make sure the project references a popular NPM package “node-sass”. No additional wiring is needed to use it in React.

Execute the following command in the command line:

npm install –save node-sass

If you’re new to SASS, you can find out more information and documentation here:
https://sass-lang.com/
https://sass-lang.com/documentation

One More Install

I don’t know about you, but I had some errors showing in the index.tsx file when I added my css references.  If this is the case for you, simply execute the following command from your client folder:

npm install

It appeared that some packages may have been missing for some reason, so this just ensures that all packages listed in our package.json file are installed.

Starting Up Our UI (React Development Server)

Before I discuss React’s development server, I want to dispel the misconception that running a “server” on our machine is bulky or complex.  As we saw in Part 2 of this blog post, Node allows us to create a server application with relatively little code.  The React Development Server is probably more complex than our implementation, but it’s just as light-weight.  In fact, we’ve already installed it and just need to start it!

In the command line, from your client folder, execute the following command:

npm start

When running, the React Development Server will monitor for changes in our client code, and automatically trigger browser refreshes as we code.  Even better is that running in dev mode, our code is compiled incrementally and not optimized, making each update blazing fast in comparison to compiling our project manually.

Starter Project

The create-react-app starts a project for us, and adds all of the necessary components for us.  It’s our job to modify this starter project into the output we need.

 

React Development Server Setup

Now you might be thinking that we have our own server implementation.  How do we get the benefits of the dev server to work with our server implementation?  This is surprisingly simple!  We just need to inform the dev server where our server application is running, and it will proxy our API calls to it.

Just to be clear, the React Development Server will still be serving our output files during development.  This is fine for now, but in production we’ll have our server application serve our complete application as intended.

In the package.json file, add the following line: 

"proxy": "http://localhost:3001",

Assuming that our server is still set to use port 3001, then it’s as simple as that!

Don’t forget to have your server app running. If you forgot how, just open a separate command prompt, and in the server’s root folder, execute the following command:

npm start

So Far

So far, we haven’t edited much.  I’ve included the files we’ve modified to this point below, just to keep us on the same page.

You might notice that the package.json file doesn’t separate the devDependencies from the dependencies list as we did in the server project.  I’ve chosen not to make the separation here, because this is how create-react-app starts the project.  On a typical project, however, I do separate them to stick with NPM’s standard conventions.

package.json
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "@types/jest": "^24.0.0",
    "@types/node": "^12.0.0",
    "@types/react": "^16.9.0",
    "@types/react-dom": "^16.9.0",
    "classnames": "^2.2.6",
    "node-sass": "^4.14.1",
    "primeicons": "^4.0.0",
    "primereact": "^4.2.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.3",
    "react-transition-group": "^4.4.1",
    "socket.io-client": "^2.3.0",
    "typescript": "~3.7.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

package.json Warning

Be careful when comparing package.json files.  You may have newer package references in your package.json file.  Over time, the package references in this post may become outdated or obsolete, and may not interact properly if mixed with packages of different versions. 

On the other hand, if you were to clone the GitHub repository, as mentioned above, you shouldn’t have any issues with it running.

index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'primereact/resources/themes/nova-light/theme.css';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';


ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Project Objectives

Before we start coding, let’s discuss what we’re going to do.

It’s worth noting that every component will have a TSX file for the code, and a SCSS file for styling.  We’ll be changing the  extensions of the Index and AppComponent’s existing CSS files to SCSS files, as well as their content.  We could get by with keeping them as CSS files, but it’s best to remain consistent.

Note that when we do this, it requires us to change the references to these files in their associated TSX files to reflect the new extension.

Messaging Client

We’ll create a special class to handle all communications between the server and the UI.  We will use events (via ReactiveX) to inform interested parties of new messages.  This is also where we’ll place our Socket.IO implementation.

Index Component

The index component was created for us, and we won’t do much here.  It’s the default entry into our React application.  All we really need to do is change the index.css extension to index.scss.

App Component

Our App component will manage which other components are visible, and store/supply the user’s name to the other controls.

User Name Component

We’ll need a name entry component, so we can get the user’s name when the app is opened.  It will supply the name to the App control, and then we won’t show this component again during the web session.

Message List Component

Our message list component will be be placed above the message entry component.  Its job will be to maintain and display the list of chat messages received from the server.  The Message List component will use the Messaging Client to receive message updates from the server.

Message Entry Component

The message entry component will handle user input of new messages, and send them to the server, using an implementation of the Messaging Client.

Copy shared-definitions.ts From Server Project

Let’s start by copying the shared-definitions.ts file from our server project into this one. I’ve included the source here if you’d prefer to copy it outright.

If you recall, this is the list of API endpoints, socket.io message types, and the type definition for a chat message we want to push back and forth with the server.  It includes a few helper functions needed on both ends as well.

If we change this file in either the server project or this client project, we should always remember to update the contents in the other project.  There are other ways of dealing with shared server/client content, but explanations can be lengthy and go beyond the scope of this post.

shared-definitions.ts
/* API endpoints for our server to respond to.  NOTE that these values are completely arbitrary. */
export const EP_GET_ALL_MESSAGES = '/api/get-all-messages';
export const EP_SEND_NEW_MESSAGE = '/api/send-message';

/* Socket.IO messages to respond to.  These values are also completely arbitrary. */
export const MSG_SEND_MESSAGE = 'send-message';
export const MSG_MESSAGE_RECEIVED = 'message-received';


/** The form of a chat message. */
export interface IChatMessage {
    /** Database ID of the message.
     *   NOTE: We're cheating a little here.  We want to share this file
     *   with the client, and the client can't reference MongoDB.  We pass this
     *   to the client as a string, and covert it back on the server to an ObjectID. */
    _id?: any;

    /** Date/Time that the message was made. */
    dateTime: Date;

    /** The name of the person who sent the message. */
    senderName: string;

    /** The message body. */
    message: string;
}

/** Returns a boolean value indicating whether or not a specified value
 *   is a string that holds a JSON date/time. */
export function isDateString(val: any): boolean {
    // It must be a string to check it.
    if (typeof val !== 'string') {
        return false;
    }

    // Check its pattern.
    return /d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z/.test(val);
}

/** Converts any string property that is formatted date/time, on a specified object,
 *   or nested objects, to an actual date. */
export function convertDates(target: any): void {
    // Arrays work differently.  Check this explicitly.
    if (Array.isArray(target)) {
        // Convert properties on this array.
        for (var i = 0; i < target.length; i++) {
            if (isDateString(target[i])) {
                // Convert this value.
                target[i] = new Date(target[i]);
            } else if (target[i] != null && typeof target[i] === 'object') {
                // Convert dates on this this nested item.
                convertDates(target[i]);
            }
        }

        // We can only work on non-null objects.
    } else if (target != null && typeof target === 'object') {
        // Check the properties on the target.
        for (var n in target) {
            if (isDateString(target[n])) {
                // Convert it.
                target[n] = new Date(target[n]);
            } else if (typeof target[n] === 'object' && target[n] != null) {
                // Convert nested values on this object.
                convertDates(target[n]);
            }
        }
    }

}

Message Client

The message client employs two methods of communications with our server.  Real-time updates happen through the socket.io-client interface, and we perform API calls with the fetch API of the browser.

Type definitions for socket.io-client are a little wonky, so I’m using a work around to specify the type of the socket property.  It appears that the type definitions for socket.io-client only provide the return type of the default method, which is io().  For whatever reason, they don’t provide those definitions by themselves.  Using the ReturnType<typeof io> syntax tells TypeScript that our property is the type of whatever is returned by io().

You’ll notice that we’re using the onMessageReceivedSubject Subject and onMessageReceived Observable for event handling.  While it’s possible to just expose a Subject for observers to be notified of events, this would give them access to the Subject’s .next() method, which is what fires the events.  To restrict the ability to fire events to just the MessageClient class, we hide the Subject and provide a public Observable that they can subscribe to instead.

Observable property/variable names commonly end with $, by convention.  I did not do this here, since it’s confusing to newcomers, but it’s good to be aware of this.

Browser Fetch API

For those who are new to the fetch command in browsers, it’s very similar to the use of XMLHttpRequest objects. In fact, you can still use XMLHttpRequests if you wish.  Fetch is way more convenient though, because it lets you get away with just defining what to do with the result, and not what to do with events.

You can get more information about Fetch here from MDN.

 

message.client.ts
import { IChatMessage, EP_GET_ALL_MESSAGES, EP_SEND_NEW_MESSAGE, MSG_MESSAGE_RECEIVED, convertDates } from "./shared-definitions";
import io from 'socket.io-client';
import { Subject } from 'rxjs';

/** This class is responsible for all server interactions concerning messaging.
 *   Consumers of this class will be able to respond to its events. */
export class MessageClient {
    constructor() {
        // Initialize our socket for messaging.
        this.initializeSocket();
    }

    private initializeSocket(): void {
        // Initialize the socket so it will communicate with the server.
        this.socket = io('/');

        this.socket.open();

        // Add a handler for responding to new chat messages.
        this.socket.on(MSG_MESSAGE_RECEIVED, (message: IChatMessage) => {
            // Convert any date strings on this object to dates.
            convertDates(message);

            // Fire the event to inform observers that we got a new chat message.
            this.onMessageReceivedSubject.next(message);
        });
    }

    /** Private subject used to trigger events when receiving new chat messages. */
    private onMessageReceivedSubject = new Subject<IChatMessage>();

    /** Observable that fires when a new chat message is received from the server. 
     *   NOTE: By convention, observables typically end in $, but I didn't do this to avoid confusion. */
    readonly onMessageReceived = this.onMessageReceivedSubject.asObservable();

    /** The socket.io client we interact with.  This type syntax may
     *   look funny, but I think the type definitions are flawed, not emitting the
     *   type references themselves, but the function's return type.  This syntax
     *   is defining our field to be the same type as returned by the io() method.
     */
    private socket: ReturnType<typeof io>;

    /** Closes our socket connection with the server.  This should only be called when we're done with
     *   as there is no way to re-open the connection without creating a new instance. */
    close(): void {
        this.socket.close();
    }

    /** Wraps a basic server (POST) request in a promise, and returns the results from the body.  Arbitrary
     *   data may be sent with the request's body, if necessary. */
    private performRequest<T>(path: string, requestData?: object): Promise<T> {
        // If we have request data, then serialize it to JSON.
        let requestBody: string | null = null;

        if (requestData) {
            requestBody = JSON.stringify(requestData);
        }

        // Execute our request.  When it returns, parse and return the JSON object.  This is actually a Promise.
        return fetch(path, { method: 'POST', body: requestBody })
            .then(response => {
                return response.text().then(resultText => {
                    if (resultText.length > 2) {
                        let result = JSON.parse(resultText);
                        // Convert date strings on this object to dates.
                        convertDates(result);
                        return result;
                    } else {
                        return null;
                    }
                });
            });
    }

    /** Returns all messages from the server, in order of it's creation date/time. */
    getAllMessages(): Promise<Array<IChatMessage>> {
        return this.performRequest(EP_GET_ALL_MESSAGES);
    }

    /** Sends a new chat message to the server, and returns a promise that resolves when complete. */
    sendMessage(message: IChatMessage): Promise<void> {
        return this.performRequest(EP_SEND_NEW_MESSAGE, message);
    }
}

Message List Component

This component will simply list existing messages, and update the list when the server gets new ones.  An added feature to this is that the list will automatically scroll to the bottom when the component is mounted and when new messages are received from the server.  This functionality uses React’s “Ref” features to reference elements within our markup.

We’ll add the files MessageList.tsx and MessageList.scss to the src folder.  The source for both is below.

You’ll notice that I define an empty IMessageListProps interface, which we could easily do without.  I keep it though, because it’s convenient if I decide to add a property in the future.  Further, because I create my react components with code snippets (in VS Code), it comes at no additional effort.

MessageList.tsx
import React from 'react';
import { IChatMessage } from './shared-definitions';
import './MessageList.scss';
import { MessageClient } from './message.client';
import { Subscription } from 'rxjs';
import { Card } from 'primereact/card';

interface IMessageListState {
    /** Holds all messages from the server, to show in the current view. */
    messages: Array<IChatMessage>;
}

export interface IMessageListProps { }

export class MessageList extends React.Component<IMessageListProps, IMessageListState>{
    constructor(props: IMessageListProps) {
        super(props);

        // Initialize the state for this component.  We'll get the message
        //  list in the mount event.
        this.state = { messages: [] };
    }

    /** Called on all React Components after they've been created and initialized. */
    componentDidMount(): void {
        // Initialize the MessageClient here (not in the constructor).
        //  This avoids attempts to update our display before it's fully setup.
        this.messageClient = new MessageClient();

        // Subscribe to its events.  Remember to keep the returned unsubscribe function
        //  so we can cleanup this component later, if it ever gets unmounted.
        this.eventSubscription = this.messageClient.onMessageReceived.subscribe(newMessage => {
            // React does not allow us to edit state objects directly.  We need to copy it, and
            //  make changes to our copy.  NOTE: It's ok to reuse state properties if they and
            //  their children are not changed.
            let newMessages = this.state.messages.slice();
            newMessages.push(newMessage);

            // Add this message to our messages list.  Though, the state only contains
            //  a messages property, using this pattern for all components every time
            //  makes future additions a trivial routine.
            //  IMPORTANT NOTE: This is a function, that returns an object!  This is why the braces are
            //   surrounded by parenthesis.
            this.setState(prevState => ({ ...prevState, messages: newMessages }), () => {
                // Wait a brief time for everything to update before we perform our scroll.
                //  If not, things may not be updated, and we won't scroll the full length.
                setTimeout(() => {
                    this.messageListRef.current.scroll({ top: this.messageListRef.current.scrollHeight, behavior: 'smooth' });
                });
            });
        });

        // Get all of the messages from the server.
        this.messageClient.getAllMessages().then(result => {
            // Set the messages in our state.  This will trigger the component to update.
            this.setState(prevState => ({ ...prevState, messages: result }), () => {
                // Wait a brief time for everything to update before we perform our scroll.
                //  If not, things may not be updated, and we won't scroll the full length.
                setTimeout(() => {
                    this.messageListRef.current.scroll({ top: this.messageListRef.current.scrollHeight, behavior: 'smooth' });
                });
            });
        });
    }

    /** Reference to the component's outer-most div element. */
    private messageListRef = React.createRef<HTMLDivElement>();

    /** Called on all React Components when they are about to be removed/destroyed. */
    componentWillUnmount(): void {
        // Cleanup our event handler (subscription).
        this.eventSubscription.unsubscribe();
    }

    /** The subscription to message events on our messageClient.
     *   This should be unsubscribed from when this component is unmounted. */
    private eventSubscription: Subscription;

    /** The MessageClient that handles all server communications for us. */
    private messageClient: MessageClient;

    /** This is the standard render method for all React Component classes, which
     *   returns the way our component looks in the browser. */
    render(): React.ReactNode {
        return (<div className="MessageList" ref={this.messageListRef}>
            {this.state.messages.map(m => <div key={m._id} className="card-wrapper">
                <Card>
                    <div className="message-wrapper">
                        <div className="message-header"> {m.dateTime.toLocaleTimeString()}: {m.senderName} </div>
                        <div className="message-body"> <pre>{m.message}</pre> </div>
                    </div>
                </Card>
            </div>)}
        </div>)
    }
}
MessageList.scss
.MessageList {
    overflow-y: auto;

    .p-card {
        border-radius: 5px;
    }

    .card-wrapper {
        margin: 10px;

        .message-wrapper {
            display: grid;
            grid-template-columns: auto 1fr;
            text-align: left;

            .message-header {
                grid-column: 1;
                font-weight: bolder;
                align-self: center;
            }
            .message-body {
                grid-column: 2;
                margin: 5px 0 5px 10px;
                align-self: center;

                pre {
                    margin: 0;
                }
            }
        }
    }
}

NewMessage Component

Next, we’ll create the NewMesage component. As you might guess, it’s purpose is to allow the user create and send a new chat message.

NewMessage.tsx
import React from 'react';
import './NewMessage.scss';
import { InputTextarea } from 'primereact/inputtextarea';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';
import { MessageClient } from './message.client';
import { IChatMessage } from './shared-definitions';

interface INewMessageState {
    /** The message being typed by the user, and sent to others when the
     *   user presses the send button. */
    newMessage: string;
}
export interface INewMessageProps {
    /** The name of the user.  This is passed to the control by the App component. */
    userName: string;
}

export class NewMessage extends React.Component<INewMessageProps, INewMessageState>{
    constructor(props: INewMessageProps) {
        super(props);

        // Initialize our state, so we have a newMessage to display (blank string).
        this.state = { newMessage: '' };
    }

    /** Called when the user changes text in our text input.  This updates our
     *   application state to reflect their changes. */
    private onMessageChanged(newValue: string): void {
        // Simply update the state on this control.
        this.setState(prevState => ({ ...prevState, newMessage: newValue }));
    }

    /** Called when the user presses the send button. */
    private onSendClicked(): void {
        // We could do this better, but keeping it simple, we'll create a client just to
        //  send our message in this scope.  NOTE: We're not subscribing to any events,
        //  so we're not going to bother closing it.
        const client = new MessageClient();

        // Create the new message to send to the server.
        let newMessage: IChatMessage = {
            dateTime: new Date(Date.now()),
            senderName: this.props.userName,
            message: this.state.newMessage
        }

        // We could block the page or something while the request is sent,
        //  but we won't do that today.
        client.sendMessage(newMessage);

        // Now that the message is sent, clear it so the user can type another one.
        this.setState(prevState => ({ ...prevState, newMessage: '' }));
    }

    render(): React.ReactNode {
        return (<div className="NewMessage">
            <Card>
                <div className="control-layout">
                    <InputTextarea cols={80} rows={4} value={this.state.newMessage} onChange={e => this.onMessageChanged((e.target as HTMLTextAreaElement).value)} />
                    <Button label="Send" onClick={e => this.onSendClicked()} />
                </div>
            </Card>
        </div>)
    }
}
NewMessage.scss
.NewMessage {
    margin: 10px 8px 0 8px;

    .control-layout {
        display: flex;
        flex-direction: column;

        button {
            margin-top: 10px;
        }
    }
}

UserNameInput Component

We’ll need a place the user can enter their name. Our application will show this component when the user arrives at our site, and after they enter their name, it will disappear.

The actual user name for the application is stored in the App component’s state.  This component’s job is to collect the name, and tell the App component what it is when the user is done.

We do this by having the App component pass a callback function to the UserNameInput component as a property.  The UserNameInput component will use that call back to send it to the App component, indicating it’s done.

 

UserNameInput.tsx
import React from 'react';
import './UserNameInput.scss';
import { InputText } from 'primereact/inputtext';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';

interface IUserNameInputState {
    /** User name the user shown in our text input. */
    newUserName: string;
}
export interface IUserNameInputProps {
    /** Callback used to tell the App component that the user has set their name, and we're done here. */
    userNameEnteredCallback: (newName: string) => void;
}

export class UserNameInput extends React.Component<IUserNameInputProps, IUserNameInputState>{
    constructor(props: IUserNameInputProps) {
        super(props);

        /** Initialize the state. */
        this.state = { newUserName: '' };
    }

    /** Called when our user name input is changed (i.e. the user is typing) */
    onUserNameChanged(newName: string): void {
        // Update our state.
        this.setState(prevState => ({ ...prevState, newUserName: newName }));
    }

    /** Called when the user clicks the OK button. */
    private onOkClicked(): void {
        // Inform the App component of the name that the user entered.
        this.props.userNameEnteredCallback(this.state.newUserName);
    }

    render(): React.ReactNode {
        return (<div className="UserNameInput">
            <Card>
                <div className="name-field">
                    <label>Your Name</label>
                    <InputText value={this.state.newUserName} onChange={e => this.onUserNameChanged((e.target as HTMLInputElement).value)} />
                </div>

                <Button label="OK" onClick={e => this.onOkClicked()} />
            </Card>
        </div>)
    }
}
UserNameInput.scss
.UserNameInput {
    label {
        margin-right: 15px;
    }
}

App Component

This component will house the entire application.  It will be responsible for showing and hiding the user name input and the message list, based on the viewType in its state.

We’re going to change the App Component considerably, and use a SCSS file instead of CSS.  This just helps keep things consistent.

Start by changing the name of the App.css file to App.scss.  Then you can paste the following snippets for each file.

App.tsx
import React from 'react';
import './App.scss';
import { UserNameInput } from './UserNameInput';
import { MessageList } from './MessageList';
import { NewMessage } from './NewMessage';

export type AppViewTypes = 'user-name' | 'messages';

interface IAppState {
  /** Controls which view to show to the user. */
  viewType: AppViewTypes;

  /** The name our user entered in the UserNameInput component. */
  userName?: string;
}

export interface IAppProps { }

export default class App extends React.Component<IAppProps, IAppState>{
  constructor(props: IAppProps) {
    super(props);
    // Initialize the state.
    this.state = { viewType: 'user-name' };
  }

  /** Callback for the UserNameInput to tell us that the user has entered a name and we're ready to show messages. */
  private onNameUpdated(newName: string): void {
    // Set the user name in our state.  Then, change our view to show messages.
    this.setState(prevState => ({ ...prevState, userName: newName, viewType: 'messages' }));
  }

  render(): React.ReactNode {
    // Here, we'll show the appropriate view, based on the viewType in our state.
    return (

      <div className="App">

        <div className="app-wrapper">
          {this.state.viewType === 'messages'
            ? <React.Fragment>
              		<MessageList />
              		<NewMessage userName={this.state.userName} />
          	</React.Fragment>
            : <UserNameInput userNameEnteredCallback={newName => this.onNameUpdated(newName)} />}

        </div>
      </div>
    );
  }
}

App.scss
.App {
  width: 100vw;
  height: 100vh;
  background-color: lightblue;

  .app-wrapper {
    display: flex;
    flex-direction: column;
    overflow-y: hidden;
    height: 100%;

    max-width: 600px;
    width: 100%;
    margin-left: auto;
    margin-right: auto;
  }
}

index.tsx

This is the default entry into our React application. We’ll leave the TSX file almost exactly the same, but we’ll be changing the CSS to a SCSS extension, and its content.

This requires us to change the reference in the index.tsx file to reflect the new extension, so don’t forget that part.

Otherwise, just use my code snippet below for the content in the index.scsss file.

You might have noticed that this is not a component file, and might even be wondering how it works.  It basically instructs React to render our App component in place of the div element named “root” in our index.html file.  The last part of this is for setting up a PWA.

A PWA (Progressive Web App), is the term given to a web application that can be run offline and even installed on your device.  The full explanation is beyond the scope of this tutorial, but as the code comment implies, the change in that single line of code will make many of these things work out of the box.  Be aware that certain details exist to make this work in production however, so be sure to read up more on PWAs before making this implementation.

index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.scss';
import App from './App';
import * as serviceWorker from './serviceWorker';

import 'primereact/resources/themes/nova-light/theme.css';
import 'primereact/resources/primereact.min.css';
import 'primeicons/primeicons.css';


ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
index.scss
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-y: hidden;
}

Run it!

Ready to run it?!  Let’s do it!

First, be sure you have our server application running.  In a command prompt, go to the root folder of the server project, and type npm start

Then, in another command prompt window, go to the root folder of our client project, and execute the same command: npm start

React should be redirecting the API calls to our server app, and serving the web pages for us!

Production Mode

Ok, so let’s try running this in production mode.  Basically, we need to create the production bundle, and then use our server application instead of the React Development Server.

To create the production bundle, just execute the following command in the client’s root folder.
npm run build

This will produce a “build” folder inside the client project.  This is the output you’d place on the server, if you were to actually deploy the client.  Our server project already points to this folder, assuming that you didn’t change the setting.

If you’re running the server app, you just need to direct your browser to the app.  Assuming you followed my examples in Part 2, you should be able to open the UI at:
http://localhost:3001

 

Finishing Touches & Conclusion

This is the end of the tutorial, but there are many things you can do to improve the app.  For starters, we have almost zero error checking on our inputs, which would be necessary in the real world.

Socket.IO has the ability to add “rooms” so you could create separate chat rooms.  There is also no way to clear the chat log, so you might implement some administrative features for this.  This almost sounds scary, but you could copy and modify many of functions already in this example to get where you needed to go!

About Intertech

Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.