We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

How to build a live code playground with React

  • Ayooluwa Isaiah
February 26th, 2019
You will need Node 6+ installed on your machine.

In this tutorial, we’ll go through how to build a code editor with React, while syncing the changes made in realtime across all connected clients with Pusher Channels. You can find the entire source code for the application in this GitHub repository.

Prerequisites

You need to have experience with building React and Node.js applications to follow through with this tutorial. You also need to have Node.js (version 6 or later) and npm installed on your machine. Installation instructions for Node.js can be found on this page.

Set up the server

Create a new directory for this project on your machine and cd into it:

    mkdir code-playground
    cd code-playground

Next, initialize a new Node project by running the command below. The -y flag allows us to accept all the defaults without being prompted.

    npm init -y

Next, install the dependencies we’ll be using to set up the Node server:

    npm install express body-parser dotenv cors pusher --save

Once the dependencies have been installed, create a new server.js file in the root of your project directory and paste in the following code:

    // server.js

    require('dotenv').config({ path: '.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Pusher = require('pusher');

    const app = express();

    app.use(cors())
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());

    app.set('port', process.env.PORT || 5000);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running → PORT ${server.address().port}`);
    });

Save the file and create a .env file in the root of your project directory. Change its contents to look like this:

    // .env

    PORT=5000

Set up Channels integration

Head over to the Pusher website and sign up for a free account. Select Channels apps on the sidebar, and hit Create Channels app to create a new app. Once your app is created, retrieve your credentials from the API Keys tab, then add the following to the .env file:

    // .env

    PORT=5000
    PUSHER_APP_ID=<your app id>
    PUSHER_APP_KEY=<your app key>
    PUSHER_APP_SECRET=<your app secret>
    PUSHER_APP_CLUSTER=<your app cluster>

Next, initialize the Pusher SDK within server.js:

    require('dotenv').config({ path: '.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Pusher = require('pusher');

    const app = express();

    const pusher = new Pusher({
      appId: process.env.PUSHER_APP_ID,
      key: process.env.PUSHER_APP_KEY,
      secret: process.env.PUSHER_APP_SECRET,
      cluster: process.env.PUSHER_APP_CLUSTER,
      useTLS: true,
    });

    app.use(cors())
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());

    app.set('port', process.env.PORT || 5000);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running → PORT ${server.address().port}`);
    });

Set up the React application

Make sure you have the create-react-app package installed globally on your machine. Otherwise, run, npm install -g create-react-app.

Next, run the following command to bootstrap your React app:

    create-react-app client

Once the command above has finished running, cd into the newly created client directory and install the other dependencies which we’ll be needing for our app’s frontend:

    npm install pusher-js axios pushid react-codemirror2 codemirror --save

Now, you can run npm start from within the client directory to start the development server and navigate to http://localhost:3000 in your browser.

Add the styles for the app

Before we tackle the application logic, let’s add all the styles we need to create the code playground. Within the client directory, locate src/App.css and change its contents to look like this:

    // client/src/App.css

    html {
      box-sizing: border-box;
    }

    *, *::before, *::after {
      box-sizing: inherit;
      margin: 0;
      padding: 0;
    }

    .playground {
      position: fixed;
      top: 0;
      bottom: 0;
      left: 0;
      width: 600px;
      background-color: #1E1E2C;
    }

    .code-editor {
      height: 33.33%;
      overflow: hidden;
      position: relative;
    }

    .editor-header {
      height: 30px;
      content: attr(title);
      display: flex;
      align-items: center;
      padding-left: 20px;
      font-size: 18px;
      color: #fafafa;
    }

    .react-codemirror2 {
      max-height: calc(100% - 30px);
      overflow: auto;
    }

    .result {
      position: fixed;
      top: 0;
      right: 0;
      bottom: 0;
      left: 600px;
      overflow: hidden;
    }

    .iframe {
      width: 100%;
      height: 100%;
    }

Render the code playground

Open up client/src/App.js and change it to look like this:

    // client/src/App.js

    import React, { Component } from 'react';
    import { Controlled as CodeMirror } from 'react-codemirror2';
    import Pusher from 'pusher-js';
    import pushid from 'pushid';
    import axios from 'axios';

    import './App.css';
    import 'codemirror/lib/codemirror.css';
    import 'codemirror/theme/material.css';

    import 'codemirror/mode/htmlmixed/htmlmixed';
    import 'codemirror/mode/css/css';
    import 'codemirror/mode/javascript/javascript';

    class App extends Component {
      constructor() {
        super();
        this.state = {
          id: '',
          html: '',
          css: '',
          js: '',
        };
      }

      componentDidUpdate() {
        this.runCode();
      }

      componentDidMount() {
        this.setState({
          id: pushid(),
        });
      }

      runCode = () => {
        const { html, css, js } = this.state;

        const iframe = this.refs.iframe;
        const document = iframe.contentDocument;
        const documentContents = `
          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <meta http-equiv="X-UA-Compatible" content="ie=edge">
            <title>Document</title>
            <style>
              ${css}
            </style>
          </head>
          <body>
            ${html}

            <script type="text/javascript">
              ${js}
            </script>
          </body>
          </html>
        `;

        document.open();
        document.write(documentContents);
        document.close();
      };

      render() {
        const { html, js, css } = this.state;
        const codeMirrorOptions = {
          theme: 'material',
          lineNumbers: true,
          scrollbarStyle: null,
          lineWrapping: true,
        };

        return (
          <div className="App">
            <section className="playground">
              <div className="code-editor html-code">
                <div className="editor-header">HTML</div>
                <CodeMirror
                  value={html}
                  options={{
                    mode: 'htmlmixed',
                    ...codeMirrorOptions,
                  }}
                  onBeforeChange={(editor, data, html) => {
                    this.setState({ html });
                  }}
                />
              </div>
              <div className="code-editor css-code">
                <div className="editor-header">CSS</div>
                <CodeMirror
                  value={css}
                  options={{
                    mode: 'css',
                    ...codeMirrorOptions,
                  }}
                  onBeforeChange={(editor, data, css) => {
                    this.setState({ css });
                  }}
                />
              </div>
              <div className="code-editor js-code">
                <div className="editor-header">JavaScript</div>
                <CodeMirror
                  value={js}
                  options={{
                    mode: 'javascript',
                    ...codeMirrorOptions,
                  }}
                  onBeforeChange={(editor, data, js) => {
                    this.setState({ js });
                  }}
                />
              </div>
            </section>
            <section className="result">
              <iframe title="result" className="iframe" ref="iframe" />
            </section>
          </div>
        );
      }
    }

    export default App;

We’re making use of react-codemirror2, a thin wrapper around the codemirror package for our code editor. We have three instances here, one for HTML, another for CSS and the last one for JavaScript.

Once the code in any one of the editors is updated, the runCode() function is triggered and the code is executed and rendered in an iframe.

Sync updates in realtime with Pusher

Let’s make it possible for multiple collaborators to edit and preview the code at the same time. We can do this pretty easily with Channels.

First, return to the server.js file you created earlier and add the following code into it :

    // server.js

    //beginning of the file
    app.use(bodyParser.json());

    app.post('/update-editor', (req, res) => {
      pusher.trigger('editor', 'code-update', {
       ...req.body,
      });

      res.status(200).send('OK');
    });

    // rest of the file

We’ll make a POST request to this route from the application frontend and pass in the contents of each of the code editors in the request body. We then trigger a code-update event on the editor channel each time a request is make to this route.

For this to work, we need to subscribe to the editor channel and listen for the code-update event on the frontend.

Let’s do just that in App.js:

    // client/src/App.js

    // beginning of the file

    class App extends Component {
      constructor() {
        super();
        this.state = {
          id: "",
          html: "",
          css: "",
          js: ""
        };

        this.pusher = new Pusher("<your app key>", {
          cluster: "<your app cluster>",
          forceTLS: true
        });

        this.channel = this.pusher.subscribe("editor");
      }

      componentDidUpdate() {
        this.runCode();
      }

      componentDidMount() {
        this.setState({
          id: pushid()
        });

        this.channel.bind("code-update", data => {
          const { id } = this.state;
          if (data.id === id) return;

          this.setState({
            html: data.html,
            css: data.css,
            js: data.js,
          });
        });
      }

      syncUpdates = () => {
        const data = { ...this.state };

        axios
          .post("http://localhost:5000/update-editor", data)
          .catch(console.error);
      };

      // rest of the file
    }

    export default App;

Then update the render function as follows:

    // client/src/App.js

      render() {
        const { html, js, css } = this.state;
        const codeMirrorOptions = {
          theme: "material",
          lineNumbers: true,
          scrollbarStyle: null,
          lineWrapping: true
        };

        return (
          <div className="App">
            <section className="playground">
              <div className="code-editor html-code">
                <div className="editor-header">HTML</div>
                <CodeMirror
                  value={html}
                  options={{
                    mode: "htmlmixed",
                    ...codeMirrorOptions
                  }}
                  onBeforeChange={(editor, data, html) => {
                    this.setState({ html }, () => this.syncUpdates()); // update this line
                  }}
                />
              </div>
              <div className="code-editor css-code">
                <div className="editor-header">CSS</div>
                <CodeMirror
                  value={css}
                  options={{
                    mode: "css",
                    ...codeMirrorOptions
                  }}
                  onBeforeChange={(editor, data, css) => {
                    this.setState({ css }, () => this.syncUpdates()); // update this line
                  }}
                />
              </div>
              <div className="code-editor js-code">
                <div className="editor-header">JavaScript</div>
                <CodeMirror
                  value={js}
                  options={{
                    mode: "javascript",
                    ...codeMirrorOptions
                  }}
                  onBeforeChange={(editor, data, js) => {
                    this.setState({ js }, () => this.syncUpdates()); // update this line
                  }}
                />
              </div>
            </section>
            <section className="result">
              <iframe title="result" className="iframe" ref="iframe" />
            </section>
          </div>
        );
      }

In the class constructor, we initialized the Pusher client library and subscribed to the editor channel. In the syncUpdates() method, we’re making a request to the /update-editor route that was created earlier. This method is triggered each time a change is made in any of the code editors.

Finally, we’re listening for the code-update event in componentDidMount() and updating the application state once the event is triggered. This allows code changes to be synced across all connected clients in realtime.

Before you test the app, make sure to kill the server with Ctrl-C (if you have it running), and start it again with node server.js so that the latest changes are applied.

Wrap up

You have now learned how easy it is to create a code playground with realtime collaboration features with Pusher Channels.

Thanks for reading! Remember that you can find the source code of this app in this GitHub repository.

Clone the project repository
  • Collaboration
  • JavaScript
  • Node.js
  • React
  • Channels

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.