Let Users Share Their Online Status in Your App with Express, React, and Twilio Sync

June 17, 2021
Written by
Reviewed by
Diane Phan
Twilion

syncreact.png

Twilio Sync is the very cool technology that underpins other Twilio APIs, such as Twilio Conversations. It allows you to add cloud-stored shared state in your app, opening up totally new opportunities for real-time collaboration and connection.

In this article, you’ll be learning how to use Twilio Sync to show who’s online in your app.

Prerequisites

Before you get started with this tutorial, make sure you’ve got the following:

Set up your app and environment

This app has two parts: a frontend and a backend. The frontend is the part of your app that your users will actually interact with. Your frontend will use the Twilio Sync JavaScript SDK, which requires a special Access Token in order to get permission to interact with the Sync API. Your backend is responsible for generating this Access Token.

You’ll start with the backend, but first, it’s time to set up your file structure and app environment.

Open your terminal or command prompt and run the following command to create a new directory that will be the parent directory to both your frontend and backend:

mkdir react-sync

Navigate into your new directory:

cd react-sync

The next step is to setup your backend, which you’ll build with Express:

mkdir token-service
cd token-service
npm init -y
npm install express twilio dotenv cors

These commands will create a new Node.js app and install four dependencies:

  • express, to build your server
  • twilio, to make use of the Twilio Node Helper Library and generate an Access Token
  • dotenv, to load your environment variables
  • cors, to enable your React.js frontend to make requests to your Express app

Create two new files, .env and index.js, inside your new token-service directory by using the following command or create them directly with your text editor:

touch .env index.js

The .env file is where you’ll add your environment variables, and the index.js file is where you’ll write the code for your backend.

Get your Twilio credentials

Open your new .env file in your favorite text editor.

Copy and paste the following variable declarations into the file:

TWILIO_ACCOUNT_SID=XXXXX
TWILIO_API_KEY=XXXXX
TWILIO_API_SECRET=XXXXX
SYNC_SERVICE_SID=XXXXX

The XXXXX strings represent placeholder values for your Twilio credentials. You’ll gather these credentials now and add them to your .env file.

Account SID

Navigate to the Twilio Console and find your Account SID. Copy and paste this value into your .env file for the TWILIO_ACCOUNT_SID variable.

API Key and API Secret

Next, visit the API Key section of the Twilio Console. Click the red plus sign to create a new API key. Give your key a recognizable name in the Friendly Name text field and leave the Key Type as Standard. Click the button that says Create API Key.

On the following screen you’ll be shown several pieces of data, including an SID and SECRET. The SID is your API key.

Copy the SID and paste it into your .env file as the value for TWILIO_API_KEY. Then, copy the value for the SECRET into your .env file as the value for TWILIO_API_SECRET.

You may want to make an additional backup of these values, because once you leave this screen, you won’t be able to access your SECRET again.

Sync Service SID

Head to the Sync section of the Twilio Console. Create a new Sync service by clicking the Create new Sync Service button. Give the service a friendly name, like “react-sync”, when prompted. On the next screen, copy the Service SID on the right side of the screen.

Screenshot of sync service configuration page with red rectangle blurring out a service SID

Paste this value into your .env file for the SYNC_SERVICE_SID variable.

Save and close this file, you’re done with your app setup!

Build the token service

Open your new index.js file in your favorite text editor and add the following code:

const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();
app.use(express.urlencoded({ extended: false }));
app.use(cors());

const port = 3000;

app.get('/token', cors({origin:'http://localhost:3001'}), (req, res) => {
  // generate access token here
});

app.listen(port, () => {
  console.log(`Token Service listening at http://localhost:${port}`)
});

This code creates a small Express app, with a route that allows GET requests to be made to the /token endpoint from origin http://localhost:3001. When your frontend needs to connect to the Twilio Sync API, it will make its request to this endpoint. Right now, the route doesn’t have any code yet, only a comment that says generate access token here. You’ll add the code next.

Inside the /token route, add the following code below the commented line:

const AccessToken = require('twilio').jwt.AccessToken;
const SyncGrant = AccessToken.SyncGrant;

const token = new AccessToken(
  process.env.TWILIO_ACCOUNT_SID,
  process.env.TWILIO_API_KEY,
  process.env.TWILIO_API_SECRET
);

const syncGrant = new SyncGrant({
  serviceSid: process.env.SYNC_SERVICE_SID,
});

token.addGrant(syncGrant);
token.identity = req.query.identity;

res.send({
  accessToken: token.toJwt()
});

The steps described in this article for generating Access Tokens are inherently insecure. This is okay for demonstration or getting setup, but in a production app you must verify and authenticate your users before granting Access Tokens to clients. Read more about how to secure your Twilio Sync apps before releasing any apps that use Sync.

Save and close this file - you’re done with your backend!

Before moving on, go ahead and start your Express server by running the following command from your token-service directory in your terminal:

node index.js

Once it’s running you’ll see a message that says:

Token Service listening at http://localhost:3000

Leave this running in the background.

Scaffold and build out your React frontend

Open a second terminal or command prompt window, navigate to back to your react-sync directory, and use the following commands to scaffold your frontend:

npx create-react-app client
npm install twilio-sync

Once these commands are finished executing, explore the new directory that was created for you: react-sync/client. In this new folder, there will be several files and subfolders, one of which is called src.

You’ll build out two components for this project, one called App and one child component called OnlineUsers. The App component will be the primary component - this component will control your local user sign in. The OnlineUsers component will render the list of all online users and respond to events, like remote users signing in and out, for example.

The App component

Open this src folder and find the file named App.js. You’ll start there.

Delete all the code already inside of App.js and replace it with the following:

import {useState} from 'react';
import OnlineUsers from './OnlineUsers.js';
var SyncClient = require('twilio-sync');

This code imports the useState() hook from React, it imports the child component I mentioned earlier called OnlineUsers and also imports the Twilio Sync client object. Because you haven’t created OnlineUsers yet, you will see an error if your React server is already running.

Create the component structure

Your next step is to create your functional component. Paste the following code below what you just added:

function App() {
  const [identity, setIdentity] = useState('');
  const [localUser, setLocalUser] = useState(null);
  const [onlineUsersSyncList, setOnlineUsersSyncList] = useState(null);
}

export default App;

This code creates the framework for your App component, and within it, uses the useState() hook you imported above to establish three state variables for your component:

  • identity, which is used to capture the local user’s name as they type it in the form field
  • localUser, a ListItem resource, which is a special Twilio Sync object representing the local user in Sync’s shared state
  • onlineUsersSyncList, a List resource, which is an ordered collection of Sync ListItem resources, representing all online users in Sync’s shared state

Add conditional rendering to the component

Below your state declarations, inside the App component function, add the following return method:

return (
  <div className="app">
  {
    localUser && onlineUsersSyncList
    ? <OnlineUsers localUser={localUser} onlineUsersSyncList={onlineUsersSyncList} />
    : <div>
        <input 
          type="text" 
          value={identity} 
          onChange={(event) => setIdentity(event.target.value)}
          placeholder="Enter your name"></input>
        <button onClick={getAccessToken}>Connect to App and show you're online!</button>
      </div>
  }
  </div>
);

This code is responsible for dictating what’s rendered when the App component loads. It checks first to see if the localUser and onlineUsersSyncList state values are truthy, meaning that they have some value that’s not null, false, empty, or undefined.

When the component first loads, before the user signs in, these values will both be falsey. In that case, the component will render a text input field to capture the user’s name, and a button for the user to click to “sign in”.

When the user clicks the button, their client will be granted an Access Token. Once the client has the Access Token, the Sync resources mentioned above will be initialized, and their corresponding state values will be populated. All of this will happen in a new function called getAccessToken() that is called when the button is clicked. You’ll add this function in the next step.

Once the state values are updated, the component will rerender, and because the state values are now truthy, the OnlineUsers component will be rendered instead of the input field and button.

This flow does not represent a secure sign in because it does not contain any mechanism to authenticate or verify the user. This means that any anonymous user could connect and get an Access Token. I’ve done it this way in this tutorial to bypass a lot of set up and get to the parts about Twilio Sync, but do not replicate this in a production environment.

Create the getAccessToken() function inside the component

Now that your return() method is added, complete the App component by adding the getAccessToken() method I mentioned above.

Add this code inside the App component, right below the state declarations, but before the return():

const getAccessToken = async () => {
  const res = await fetch(`http://localhost:3000/token?identity=${identity}`);
  const data = await res.json();
  const syncClient = new SyncClient(data.accessToken);

  const SyncList = await syncClient.list('online-users');
  const localUser = await SyncList.push({name: identity})

  await setLocalUser(localUser);
  await setOnlineUsersSyncList(SyncList);
}

This code makes a GET request to your /token route on your Express server that’s running in the background.

When it gets the Access Token in the response from the request, an instance of SyncClient is created, and then used to create the two Sync objects: SyncList and localUser. It then assigns these objects to the values of the corresponding component state variables.

That’s a wrap for the App component! Time to move on to the OnlineUsers component.

The OnlineUsers component

Create a new file in the /client/src folder called OnlineUsers.js.

Add the following code to this new file:

import {useState, useEffect} from 'react';

This imports the useState() and useEffect() hooks from React.

Below your import add the following code to create the scaffolding of the OnlineUsers component:

Create the component structure

function OnlineUsers({localUser, onlineUsersSyncList}) {
  
}

export default OnlineUsers;

This code creates and exports an empty component, and destructures the props object from the parent App component into two ready to use variables: localUser and onlineUsersSyncList.

Inside the OnlineUsers function, establish some initial state:

function OnlineUsers({localUser, onlineUsersSyncList}) {
  const [onlineUsers, setOnlineUsers] = useState([]);
}

export default OnlineUsers;

Perform some important tasks when the component mounts

Now you can use the useEffect() hook to do a few things once the component mounts:

  • Get the current list of all online users from Sync and assign it to the onlineUsers state variable
  • Set an event listener on the onlineUsersSyncList object to listen for when another user joins
  • Set an event listener on the onlineUsersSyncList object to listen for when another user leaves

To do all of that, copy and paste the following code beneath your state declaration inside the function:

  useEffect(() => {
    getSetOnlineUsers();

    onlineUsersSyncList.on('itemAdded', event => {
      if (!event.isLocal) {
        getSetOnlineUsers();
      }
    });

    onlineUsersSyncList.on('itemRemoved', getSetOnlineUsers);

    window.addEventListener("beforeunload", removeParticipant);
  }, []);

Add your helper functions

This code makes calls to two functions you haven’t written yet: getSetOnlineUsers() and removeParticipant(). You’ll add these functions now. Below the useEffect() hook, paste in the following code:

// Gets current list of online users from Sync
const getSetOnlineUsers = async() => {
  const items = await onlineUsersSyncList.getItems();
  setOnlineUsers(items.items);
}

// Removes local user from Sync list
const removeParticipant = async () => {
  const list = await onlineUsersSyncList.remove(localUser.index)
  list.close();
}

Render the component

To finish out the component, add the return() method to render the list of users stored in the onlineUsers component state. Paste this code before the closing bracket of the OnlineUsers function component:

return (
  <div className="participants">
    <h2>Online users:</h2>
    {
      onlineUsers.map(p => <div key={p.index}>{p.data.name}</div>)
    }
  </div>
);

Save and close this file, you’re all done writing components!

Test out your new Sync app

Head back to your terminal or command prompt and make sure your backend Express server is still running on port 3000.

In your second terminal window, navigate to your client directory, run the following command:

npm start

This will start your local React server. Typically, a React server will start on port 3000, but because your backend is already running on this port, it will prompt you to confirm that running on port 3001 is okay. Press y to confirm.

After that, your server will start, and you’ll be able to see your app in your browser at http://localhost:3001.

Open your app in two different browser tabs or windows.

In one tab, type your name into the text field and click the button, you’ll see your name populate the online users list:

Screenshot showing online users list

In the second tab or window, type in a different name, and click the button. This time, you’ll see both users in the list:

Screenshot showing both users in list

Close either tab - when you do, you’ll see in the remaining tab that there is once again only one user online.

Congratulations on finishing this Twilio Sync and Express/React app! This article demonstrated just one way to use Sync. Take what you learn here and use it to build simultaneous multiplayer web games or try out building a collaborative notepad with Sync and vanilla JS. Check it out, and let me know on Twitter what you’re building!

Ashley is a JavaScript Editor for the Twilio blog. To work with her and bring your technical stories to Twilio, find her at @ahl389 on Twitter. If you can’t find her there, she’s probably on a patio somewhere having a cup of coffee (or glass of wine, depending on the time).