Improving our React workflow with ES6 and functional stateless components

Tags:

Someone once asked on Twitter about the concrete, real-world benefits of ES6 (or ES2015, as it’s officially known, despite nobody calling it that). It seems the benefits would be a bit difficult to measure: ES6 mostly just adds convenience. How does a convenience-feature benefit us?

If you think about it, convenience makes code easier to write. When we have more convenience, we don’t have to fight the language to make it do what we want, and we can spend that energy focusing on the actual problem at hand! This makes it more fun to write code, but also improves our code quality, as the code simply flows out from our fingertips, rather than having to tediously hammer at the keyboard while pulling your hair.

In this article, I’ll show you how you can start making use of ES6 features in your React project. We’re also going to look at a React feature called functional stateless components, since it’s also very convenient and goes well with ES6 syntax.

I’ll be using the React chat app we’ve been developing as the example here, but you can follow even if you’ve not read the previous articles in the React chat app series.

As usual, full code is available on Github.

What we’ll do this time

So far, the chat app can connect any number of users via WebRTC. It also happily displays all the messages users are sending to each other. There’s just a small problem: Nobody has a name! It’s a bit silly to have users without names, so we’re going to address that.

We’ll be adding a new feature to the chat app: User names. This is going to require some small changes here and there, and along the way, we’re going to add ES6 and functional stateless components into the mix.

Setting up ES6 support

Since we’re already bundling our code with Browserify, which runs it through Babel, this step is quite easy.

  • First, we need to install ES6 support for babel:

    npm install --save babel-transform-es2015

  • Then, we just update our npm build script to include the new transform:

    browserify -t [ babelify --presets [ es2015 react ] ] src/index.js -o build/app.js

    Note we added es2015 within the presets listing in addition to react

(see full package.json source)

Babel uses so-called transforms to support different language features. We’re using the react transform for JSX, and now we’ve added the es2015 transform. With that, we can now use any of the ES6 features in our code. Pretty simple.

Note for Webpack users: If you use Webpack and want to use ES6, check out Using ES6 in the browser with Babel 6 and Webpack.

Storing user identity

To add user names to the chat, we first need some way for the user to enter their name, and also a place to store it in.

We can add an input box into ConnectionForm for the user name. To store it, let’s add a simple new module: Identity.

For time being, let’s keep it basic: Just a way to easily get and set the current user’s name.

src/Identity.js

var name;
 
module.exports = {
  get: () => name,
  set: (n) => name = n
};

Here, we’re making use of ES6’s fat arrow syntax. This allows us to define a function in the form of…

(params) => expression
//is mostly same as...
function(params) { return expression }

If you’re unfamiliar with how this syntax works, I suggest you check my article on ES6 features.

So in short, the get function returns the value of name, and set sets a new value for it.

Next, let’s modify ConnectionForm

src/ConnectionForm.js

var React = require('react');
 
module.exports = React.createClass({
  getInitialState: () => ({ name: '' }),
 
  updateName: function(ev) {
    this.setState({ name: ev.target.value });
  },
 
  handleButton: function(type) {
    this.props[type](this.state.name);
  },
 
  render: function() {
    return <div>
      {this.props.connected ? 'Connected' : 'Not connected'}
      <input value={this.state.name} onChange={this.updateName} type="text" placeholder="Name" />
      <button onClick={this.handleButton.bind(this, 'onHost')}>Host</button>
      <button onClick={this.handleButton.bind(this, 'onJoin')}>Join</button>
    </div>;
  }
});

To support the name input box, we’ve added name into the state of the component. This works in a similar way as the chat message input: The name value in state is updated by the update function we set for the onChange event.

You’ll notice there’s a new handleButton function which is used for host/join handling. The reason for this is I want to pass the user name as a parameter to the callback. We can conveniently reuse this function by using bind as you can see in the JSX code. bind allows us to set a specific this value, but also partially apply a function.

Partially applying a function means we pre-define parameters for it. When we call this.handleButton.bind(this, 'onHost'), it returns a new version of handleButton, where the first parameter is set to 'onHost'. When the function is called as a result of a click, the parameter is set and the correct callback from props gets called.

We also need to update Chat to save the name from the form into the newly created Identity object.

src/Chat.js

/* unchanged requires omitted */
const Identity = require('./Identity');
 
module.exports = React.createClass({
  /* unchanged functions omitted */
 
  handleConnectionForm: function(type, name) {
    Identity.set(name);
    ConnectionManager[type]();
  },
 
  render: function() {
    return <div>
      <MessageList messages={this.state.messages} />   
      <MessageForm onSend={this.onSend} />
      <ConnectionForm
        connected={this.state.connected}
        onHost={this.handleConnectionForm.bind(this, 'host')}
        onJoin={this.handleConnectionForm.bind(this, 'join')}
        />
    </div>;
  }
});

I’ve used const here instead of var for the require. With const, we cannot change the value of Identity. We probably wouldn’t want to do that anyway, but it’s a nice way of making it clear to anyone who reads the code – “this value is not going to change” – plus, it prevents changing it accidentally.

Note: const does not prevent changing objects. It only prevents you from assigning a completely new value into the constant. For more detail, see my article on ES6 features.

The rest of the code is similar to what we did in ConnectionForm: The new handleConnectionForm is used together with bind to handle hosting and joining. The function also sets the name parameter into Identity, so it’s easily accessible.

Although Identity doesn’t have the Store suffix, its function is essentially similar to MessageStore in that it stores some data for us. Although the functions in Identity do very little now, later we could for example add validation logic into them.

Adding the sender’s name to messages

Next, we need to include the sender’s name in the message objects we send over WebRTC. Since we’ve got it conveniently saved into Identity, all we need to do is add support for it into the necessary places in our code and we’re done!

First, we need to change the message to an object. Otherwise we couldn’t really add in extra info like sender name so easily.

All we need to change for this is the MessageForm component which creates the messages.

src/MessageForm.js

module.exports = React.createClass({
    /* unchanged code omitted */
 
    submit: function(ev) {
        ev.preventDefault();
 
        this.props.onSend({
            message: this.state.input
        });
 
        this.setState({
            input: ''
        });
    }
});

Here, you’ll notice we’ve changed the parameter to this.props.onSend into an object, which has a message property for the chat message. We’ll also need to change the rendering code slightly, but we’ll do that in a bit.

Before that, in the connection manager, we want to include the identity for messages that we send:

src/ConnectionManager.js

/* unchanged requires omitted */
const Identity = require('./Identity');
 
module.exports = {
  sendMessage: function(message) {
    var packet = Object.assign({
      from: Identity.get()
    }, message);
 
    connection.send(packet);
  }
 
  /* unchanged functions omitted */
};

In this case, we’re using Object.assign to include a from property in the data we send. Using it in this way allows us to conveniently have default values for objects. The first parameter to Object.assign is the target object. Properties from all other parameters are copied into it. So, if any of the objects already contains a from property, it overrides the value we set here, but if it’s missing, the identity value is used instead.

And lastly, we need to update MessageStore to include a default as well. When adding new messages to it, messages from us doesn’t include the from value, so it would be missing for some messages otherwise.

src/MessageStore.js

const Identity = require('./Identity');
 
/* unchanged code omitted */
 
module.exports = {
  /* unchanged functions omitted */
 
  newMessage: function(message) {
    messages.push(Object.assign({
      from: Identity.get()
    }, message));
    emitter.emit('update');
  }
};

This is the same as in ConnectionManager – we use the identity value as a default when from is missing.

Updating message display code with Functional Stateless Components

The last thing we need to do is update our rendering code to take into account the new message object format.

We need to update ChatMessage since it handles displaying each message. This is a perfect opportunity to make use of React’s functional stateless components, since the ChatMessage component is extremely simple.

src/ChatMessage.js

var React = require('react');
 
module.exports = function(props) {
  return <p>{props.from}: {props.message}</p>;
};

What happened? Where did React.createClass go?

Functional stateless components allow you to use a function directly as a component. They cannot have state, but can make use of props like we do here. This makes them ideal for simple components which only need to display some data – the code was already quite straightforward, but now it’s even shorter than before!

This feature is even nicer with ES6 syntax:

var React = require('react');
 
module.exports = ({ message, from }) => <p>{from}: {message}</p>;

Here, we use both the fat-arrow syntax and destructuring syntax. This code is for most intents and purposes the same as…

module.exports = function(props) {
  var from = props.from;
  var message = props.message;
  return <p>{from}: {message}</p>;
};

As you can see here, ES6 syntax can in some cases greatly reduce the amount of code we need, and also clarify it. In the above code, the ES6 destructuring code clearly shows the function needs message and from as the two properties in its parameter object – the other version is less clear.

You can read more about destructuring from my ES6 features article.

And finally, we need to update MessageList so it passes the necessary props.
src/MessageList.js

/* unchanged code omitted */
module.exports = ({ messages }) => <div>
    {messages.map(msg => <ChatMessage {...msg} /> )}
  </div>;

I went ahead and changed MessageList into a functional stateless component as well, and made use of ES6 features to simplify the code further.

The most important thing to note here is <ChatMessage {...msg} />. The {...msg} syntax is React’s spread syntax. It works similar to the ES6 spread syntax, but with JSX – in other words, all properties from msg are passed as props into ChatMessage.

With this done, we can now npm run build our code, and if you try it out, you’ll see that messages are now associated with a user name!

One more thing

Like all the best Apple presentations, we’ve got one more thing that I want to do.

Wouldn’t it be nice if we got a message when a user joins? Then we’ll actually know who’s in the chat, so let’s add that in.

A nice way to do this would be to include a type indicator along with our messages. This way, we can send a “message” type message for when someone is talking, and a “join” type message when a new user joins.

Again because we’ve built our app very modular, we only need to do small changes to make this work. First, we need to modify MessageForm, as that’s where the “message” type messages would get created.

src/MessageForm.js

module.exports = React.createClass({
  /* unchanged code omitted */
 
  submit: function(ev) {
    ev.preventDefault();
 
    this.props.onSend({
      type: 'message',
      message: this.state.input
    });
 
    this.setState({
      input: ''
    });
  }
});

Here, we simply added a type property into the message object. This allows us to differentiate between different kinds of messages easily.

Next, we need a place where to send the join messages from. We can do this conveniently from ConnectionManager – and finally find some more use for it, beyond just relaying events.

src/ConnectionManager.js

module.exports = React.createClass({
  /* unchanged code omitted */
 
  join: function() {
    var conn = ClientConnection();
    setupConnection(conn);
 
    conn.onReady(() => {
      this.sendMessage({
        type: 'join'
      });
    });
  }
});

For joining, we now include an event handler. This handler’s job is to send the join-message. Note the ES6-arrow function – it allows us to conveniently refer to this from the callback function, instead of having to manually bind it. We don’t need to pass any other info besides the type, as the sendMessage function automatically adds our identity into the message.

Now that the join message gets sent, we can add some code to display it. Let’s add a new component, JoinMessage.

src/JoinMessage.js

var React = require('react');
 
module.exports = ({ from }) => <p>{from} has joined</p>;

This should look familiar now – another functional component, with ES6 arrow and spread functionality used.

To use this function, we’ll just update the MessageList component to pick the correct component based on the type property:
src/MessageList.js

/* unchanged code omitted */
const JoinMessage = require('./JoinMessage');
 
function createMessage(msg) {
  if(msg.type === 'message') {
    return <ChatMessage {...msg} />;
  }
  else if(msg.type === 'join') {
    return <JoinMessage {...msg} />;
  }
}
 
module.exports = ({ messages }) => <div>
    {messages.map(createMessage)}
  </div>;

We now have a function which picks between the two renderer-components, and the map is updated to use it.

If you build and run the app, you’ll now get a nice message informing of newly joined chatters!

Adding a user list

To finish things up, let’s add a component that’ll display all users in the chat.

Currently, when a user joins, everyone in the chat gets to know about it. But joining users have no idea who’s in the chat. So, it would probably be a good idea to display that information to them.

We’ve laid all the work to be able to send different kinds of messages, so we can solve this easily now. We’ll add a new message type, users, which the host will send to any new joiners.

To keep track of users, we need a place to store them. We can add a new UserStore to deal with that first:
src/UserStore.js

const EventEmitter = require('events').EventEmitter;
 
const emitter = new EventEmitter();
const users = [];
 
module.exports = {
  handleMessage: function(message) {
    if(message.type === 'join') {
      users.push(message.from);
      emitter.emit('update');
    }
    else if(message.type === 'users') {
      users.length = 0;
      users.push.apply(users, message.users);
      emitter.emit('update');
    }
  },
 
  getUsers: function() {
    return users.concat();
  },
 
  subscribe: function(callback) {
    emitter.on('update', callback);
  },
 
  unsubscribe: function(callback) {
    emitter.off('update', callback);
  }
};

The store’s basic functionality is similar to our other data stores: We’ve got a variable to store our data, a function to update it and some events so we can tell other parts of code about changes.

Here, you can see how const doesn’t prevent changing the object. Although we make users a constant, we’re still changing it within handleMessage. As long as we don’t attempt to assign a new value (ie. users = somethingElse), we’re fine.

The users.length = 0 and users.push.apply lines might look a bit weird. Setting an array’s length to 0 empties the array without replacing it with a new one, which is quite convenient as we made it a constant. Calling apply on push means we call it like users.push(a, b, c, d...). This makes use of the less commonly used functionality of push, which allows you to add multiple items to an array with a single call.

To be able to send the list of users to newly connected chatters, we first need to know when a new user connects. We can do this easily by adding a new event into the HostConnection object:

/* unchanged code omitted */
module.exports = function() {
  rtc.on('connect', function() {
    peers.push(rtc);
    emitter.emit('new-peer', rtc);
  });
 
  /* omitted code */
 
  return {
    onNewPeer: function(callback) {
      emitter.on('new-peer', callback);
    }
  };
};

The RTC connection conveniently sends a connect event when the connection is established. We’re already using it to include newly connected users into the host’s peer list, and now we can also use it to emit events for others to listen to.

With the event added, we can use it in connection manager to send out a list of users, similar to how we’re sending out a join message. We also need to add a way to send a message only to a specific user, since there’s no reason to broadcast the user listing to everyone every time someone joins – afterall, the UserStore handles updating the user listing based on join events too.

src/ConnectionManager.js

/* unchanged code omitted */
const UserStore = require('./UserStore');
 
function createPacket(message) {
  return Object.assign({
    from: Identity.get()
  }, message);
}
 
module.exports = React.createClass({
  /* unchanged code omitted */
 
  sendMessage: function(message) {
    connection.send(createPacket(message));
  },
 
  sendTo: function(peer, message) {
    peer.send(createPacket(message));
  },
 
  host: function() {
    const conn = HostConnection();
    setupConnection(conn);
    conn.onNewPeer(peer => {
      //user list does not include self, but in this case
      //the host should tell the connected peer their identity
      //as well, so we include it within the user listing
      this.sendTo(peer, {
        type: 'users',
        users: UserStore.getUsers().concat([Identity.get()])
      });
    });
  }
});

I’ve added a new createPacket function to handle assigning the identity value, so that we don’t need to copypaste it into both sender-functions.

Then, a bit similar to the join-handling, there’s a function for the onNewPeer callback. In the callback, we use the new sendTo function to send a list of users to any newly connected peer.

And last but not least, we need to add a new component for displaying the user list, and connect things together in Chat.

src/UserList.js

const React = require('react');
 
module.exports = ({ users }) => <ul>
    {users.map(u => <li>{u}</li>)}
  </ul>;

Nothing special here – we simply create a standard unordered list.

src/Chat.js

/* unchanged code omitted */
const UserList = require('./UserList');
const UserStore = require('./UserStore');
 
module.exports = React.createClass({
  getInitialState: function() {
    return {
      messages: MessageStore.getMessages(),
      connected: ConnectionManager.isConnected(),
      users: UserStore.getUsers()
  },
 
  componentWillMount: function() {
    MessageStore.subscribe(this.updateMessages);
    UserStore.subscribe(this.updateUsers);
    ConnectionManager.onStatusChange(this.updateConnection);
    ConnectionManager.onMessage(this.handleMessage);
  },
 
  componentWillUnmount: function() {
    MessageStore.unsubscribe(this.updateMessages);
    UserStore.unsubscribe(this.updateUsers);
    ConnectionManager.offStatusChange(this.updateConnection);
    ConnectionManager.offMessage(this.handleMessage);
  },
 
  handleMessage: function(message) {
    MessageStore.newMessage(message);
    UserStore.handleMessage(message);
  },
 
  updateUsers: function() {
    this.setState({
      users: UserStore.getUsers()
    });
  },
 
  render: function() {
    return <div>
      <MessageList messages={this.state.messages} />
      <UserList users={this.state.users} />
      <MessageForm onSend={this.onSend} />
      <ConnectionForm
        connected={this.state.connected}
        onHost={this.handleConnectionForm.bind(this, 'host')}
        onJoin={this.handleConnectionForm.bind(this, 'join')}
        />
    </div>;
  }
});

For this, we’ve added some new state and wired the user store into the necessary things. We’ve added a new updateUsers function to update the component state when users are updated, and made sure the user store receives each new message from ConnectionManager.

The render function also includes the new UserList component, which receives the users array from state as its users property.

Conclusion

The chat app is finally starting to have most features you’d expect – chatting, names, and a user list. You can find the full code on Github as always.

With the ES6 features added in along with functional stateless components, we’ve simplified the code in several places and added some useful protection in form of constants.

It’s still looking kind of ugly though – even more so with the user list just mixed in right in the middle like that.

So next time, we’re going to look at some ways that we can start styling our application, so it looks less clunky (ok, it might still look a bit clunky because I’m a terrible designer, but at least I make up for it with my equally terrible humor)

Sign up for my newsletter with the form below, and I’ll send you all the follow-ups right to your inbox