React application data-flow: Where and how to store your data?

Tags:

This is the third article in the series where we build a Slack-style chat app with React.

Last time we set up NPM and Browserify for our project. With the tooling set up, we can focus on building our application’s features again.

The first feature I want to add is of course having multiple chat participants. But our code is currently a bit messy. We have messages and other data all over the place – this is going to make it difficult to add the new feature (and test it later).

Let’s fix the problem: In this article, we’re going to look at some best practices on how data should be modeled in a React app. Typically, this is when we would start talking about Flux, Redux and all that, but in this case we won’t. Instead, we’re going to start by implementing a light-weight data store mechanism and use that instead. I’ll tell you why in a moment.

Basic data flow in a React app

Before we jump into the code, let’s talk about what we want to do.

The basic idea with React is whenever we have nested components – for example, Chat, which has a list of ChatMessages – the parent component updates each child.

This is what we’re doing in our code now. The Chat component has a list of messages. Each message is given to a ChatMessage, in other words, the data flows from parent (Chat) to child (ChatMessage).

We also have the form used to add new messages. The input field within the form can be thought of as child components too. In that case, the flow is different: The input sends an event, which goes back to Chat, which updates itself. Then, the data flows to the children if anything changed.

This may sound vague at first, but let’s start putting this into practice and you’ll see it makes a lot of sense.

Updating our code

As above, the basic idea is that data flows from parent to child. Let’s look at that in more detail.

Let’s change our application to work like above, so we can get a better view of how the data flows. This also gives us a good excuse to talk about some interesting benefits of this approach.

Currently our code doesn’t separate message rendering into a MessageList component. Let’s add that in.

src/MessageList.js

var React = require('react');
 
var ChatMessage = require('./ChatMessage');
 
module.exports = React.createClass({
  render: function() {
    var messages = this.props.messages.map(function(msg) {
      return <ChatMessage message={msg} />;
    });
 
    return <div>{messages}</div>;
  }
});

Like ChatMessage, this is another simple component. We just take a messages prop and create a list of messages out of it.

We need a small change to Chat to make this work. I’ve omitted unchanged parts in the code below, but you can see the full code here.

var React = require('react');
 
var MessageList = require('./MessageList');
 
module.exports = React.createClass({
  getInitialState: /* omitted */,
 
  submit: function(ev) {
    ev.preventDefault();
 
    var newMessage = this.state.input;
 
    this.setState({
      messages: this.state.messages.concat([newMessage]),
      input: ''
    });
  },
 
  updateInput: /* omitted */,
 
  render: function() {
    return <div>
      <MessageList messages={this.state.messages} />
      <form onSubmit={this.submit}>
        <input value={this.state.input} onChange={this.updateInput} type="text" />
        <input type="submit" value="Send" />
      </form>
    </div>;
  }
});

First, we load the list component as it’s required for rendering. Then, we updated the submit function so it doesn’t create a ChatMessage anymore – this is now handled within the MessageList.

Finally, in the render function, we pass the list of messages as an attribute to the MessageList. Remember that passing attributes to components like this makes them available within the this.props object.

Only the root component Chat holds any state. All the others simply take some props from their parent and render something based on them.

So what are the benefits of this approach?

  • It’s simple: When we use attributes and props, we always know where the data is coming from. I’ve worked with an AngularJS application which used databinding too much, and finding what caused a change in data was really tricky. Not here – data always comes from the parent.
  • We can easily reuse components. I’ll show you an example of this in a bit.
  • Saving data is simplified. We used to save a ChatMessage in the messages list, but now it’s just a plain string. This makes our work easier when we start sending messages between users

Since the reuse case is often talked about, but can be hard to understand how it would work, let’s look at an example.

Let’s imagine the message list actually used some kind of a mechanism to load a list of messages. Perhaps it does an ajax request or whatever – it doesn’t matter. What if we wanted to show two message listings, with different contents? That wouldn’t work, because the message list holds its own state that it fetched from somewhere.

But with a props-based approach like we have now, it’s trivial! If we want to display another message list, for example to display private messages, we’ll simply add another and give it a different set of messages!

render: function() {
  return <div>
    <MessageList messages={this.state.messages} />
 
    <MessageList messages={this.state.someOtherMessageArray} />      
 
    <form onSubmit={this.submit}>
      <input value={this.state.input} onChange={this.updateInput} type="text" />
      <input type="submit" value="Send" />
    </form>
  </div>;
}

Data-flow with events

With the changes now, we went through the first case – data flow from parent to child.

But the second case is different. We need some way to update the state in our application – If data flows from parent to child, this can’t work – or can it?

The idea is that while data flows from parent to child, events can go the other way. So, for example, for adding new messages…

In this case, the child can send back an event. We’re already using some events with the input field, but let’s look at that in a bit more detail, and refactor the form into its own component.

As with the message list, we’ll create a new file and add a component there:

src/MessageForm.js

var React = require('react');
 
module.exports = React.createClass({
  getInitialState: function() {
    return {
      input: ''
    };
  },
 
  submit: function(ev) {
    ev.preventDefault();
 
    this.props.onSend(this.state.input);
 
    this.setState({
      input: ''
    });
  },
 
  updateInput: function(ev) {
    this.setState({ input: ev.target.value });
  },
 
  render: function() {
    return <form onSubmit={this.submit}>
      <input value={this.state.input} onChange={this.updateInput} type="text" />
      <input type="submit" value="Send" />
    </form>;
  }
});

This code is similar to what we had in Chat – We simply moved the form-related logic out. You might have noticed one thing: Changing React apps is pretty easy! Often you just move things around like this, and everything else continues working. How often can you say that about JS code?

The most important change is in the submit function. Notice that we’re calling this.props.onSend.

We can pass any JS value as a prop, including functions. This means we can easily do custom “events” like onSend. The message form doesn’t care what you do with the data – saving it is left for another component.

This makes it easier to reuse or test this component. It also helps keep changes to data in a centralized location – as an example, have you ever worked with a jQuery codebase, where you have tons of selectors and things get changed who knows where? Yeah. It’s pretty hard to understand.

Instead of having data changes everywhere in our code, we want to keep them centralized. So, in this case, we can provide a custom handler for saving the data, and we avoid special logic for saving here.

Let’s change Chat to accomodate the new component:

var React = require('react');
 
var MessageList = require('./MessageList');
var MessageForm = require('./MessageForm');
 
module.exports = React.createClass({
  getInitialState: function() {
    return {
      messages: []
    };
  },
 
  onSend: function(newMessage) {
    this.setState({
      messages: this.state.messages.concat([newMessage]),
    });
  },
 
  render: function() {
    return <div>
      <MessageList messages={this.state.messages} />
      <MessageForm onSend={this.onSend} />
    </div>;
  }
});

Notice how every change makes Chat simpler. Originally we didn’t have that much code here, but as our application grows, our components get bigger. It often makes sense to start splitting them when the code gets complicated, like we’ve done here.

Since we moved the form-related logic into MessageForm, we’ve removed it here, and added the new MessageForm into render. We also pass this.onSend into the component – this function handles saving the data into the message array.

React terminology: Controller view

Chat is a so-called Controller View or a Controller Component. A controller view is a component which acts somewhat similar to controllers in MVC – they contain code to deal with the moving parts and data. In this case, the Chat controller view’s job is to keep track of the message list and handle submitting new messages.

Ideally, you want to keep state and changes to state in centralized locations like this. Our other components are reasonably simple – they mainly render something based on props. This is a concrete benefit of the approach of keeping state centralized: We can keep much of our code simple.

You can have multiple controller views in your application, but you should think before adding more: More places which hold state make finding problems with it harder. However, in some cases it makes sense to do so. Our MessageForm is another kind of controller view, as it keeps track of the form’s state. Since the form is very self-contained, it makes sense to build it this way.

Adding a data store

So far, we’ve stored the message list within the Chat component. But going forwards, we should separate it out.

We’ll soon start adding code to deal with data from other users and things like that. Putting code for that into Chat doesn’t really make sense. It’s better to create a separate place for the data, which means we can keep Chat simple, and testing the data store will be much easier.

This is typically where you’d start dealing with Flux or Redux or any of the other data store patterns within the React ecosystem. The problem with Flux in particular is that it’s fairly complex – and all we have to store is an array!

We don’t need a complicated data store pattern right now. Plus, it’s useful to understand how you would set one up manually, as that makes it easier to understand libraries like Redux in the future.

Because of this, we’ll start simple and build our own data stores out of plain JavaScript objects – it doesn’t get much simpler than that.

Let’s create a new file and call it src/MessageStore.js. We’ll put the message-related logic there. The basic idea is that the MessageStore object should keep track of messages, and send events whenever there are new ones.

var EventEmitter = require('events').EventEmitter;
 
var emitter = new EventEmitter();
 
var messages = [];
 
module.exports = {
  getMessages: function() {
    return messages.concat();
  },
 
  subscribe: function(callback) {
    emitter.addListener('update', callback);
  },
 
  unsubscribe: function(callback) {
    emitter.removeListener('update', callback);
  },
 
  newMessage: function(message) {
    messages.push(message);
    emitter.emit('update');
  }
};

First, we require the events module. This is a built-in module in Node.js, which we can use without any extra installs thanks to Browserify. The events module includes a handy EventEmitter object which we’ll use here.

We create a new EventEmitter and save it into a variable. Remember, when using Node-style modules, all variables we create are local to the module and not globals. This means we can safely store module-private things, such as the event emitter and the list of messages, simply with variables.

We assign an object with four functions into module.exports. This will be the MessageStore object we’ll use from other parts of our code.

The getMessages function returns the list of messages. Note that we’re using messages.concat(), rather than returning messages. This creates a copy of the array. The benefit of this is the code outside of this module can change the array as much as they like, and it won’t affect the array inside MessageStore. If we instead returned messages directly, if someone changes the array, then it could potentially break our code here.

If you’re unsure about how concatenating affects things, check this fiddle showing the difference.

Side note: You might’ve heard of a library called ImmutableJS. It helps us deal with the above problem in a more robust way. We’ll be looking at how it works in a later article in the series.

Next, the subscribe and unsubscribe functions are used to register and unregister event listeners. For this, we can simply make use of the EventEmitter we’ve created. The emitter has functions addListener and removeListener which can be used for this purpose. They take the name of the event we listen to, and the callback function as their parameters.

Lastly, the newMessage function is used to add new messages to the store. We update the messages array with the newly added message, and then emit an event. Note that the event’s name matches the names we used with subscribing and unsubscribing, meaning this triggers the event listeners.

Now that we’ve got a place to keep messages outside of the Chat component, let’s update the component to use MessageStore.

/* other requires omitted */
var MessageStore = require('./MessageStore');
 
module.exports = React.createClass({
    getInitialState: function() {
        return {
            messages: MessageStore.getMessages()
        };
    },
 
    componentWillMount: function() {
        MessageStore.subscribe(this.updateMessages);
    },
 
    componentWillUnmount: function() {
        MessageStore.unsubscribe(this.updateMessages);
    },
 
    updateMessages: function() {
        this.setState({
            messages: MessageStore.getMessages()
        });
    },
 
    onSend: function(newMessage) {
        MessageStore.newMessage(newMessage);
    },
 
    render: /* unchanged, omitted */
});

The first change of note is in getInitialState we load the list of messages. This function is called once when the component is created, so it makes sense to get the current list of messages at this point.

Since the list of messages can update, we need to register event listeners. A good place to do this is in the componentWillMount and componentWillUnmount lifecycle methods. Lifecycle methods, including getInitialState, get called at certain points in the component’s “life cycle”. The will mount and will unmount methods get called when the component is about to be rendered into DOM, and when it is about to be removed from DOM, respectively.

Registering event handlers in componentWillMount ensures any updated data gets reflected in the component’s state. When we register a listener, it’s also a good idea to ensure it gets unregistered when it’s no longer needed. componentWillUnmount is the perfect place to unregister event handlers, as any component which gets removed from the DOM no longer needs to be updated with events.

We register the updateMessages function as a subscriber to MessageStore. This function simply updates the component’s internal state by grabbing the list of messages from the store. You might notice we’re using this quite carelessly here – if you’ve used this, I’m sure you’ve ran into problems where it doesn’t point at what you think. React saves us from some trouble by automatically binding this into the component. Thanks to this (pun intended) we can safely pass around the this.updateMessages function.

Lastly, we’ve updated the onSend function to use the MessageStore.newMessage function. We no longer need to update the internal state here, as by adding a message to the store, we trigger the event handling – which in turn triggers our component’s updateMessages handler and updates the state.

This kind of flow is typical to React:

  1. You have some kind of a data store object, such as MessageStore. Depending on your application’s needs, this could also be a Flux or Redux data store.
  2. A component triggers an update in the data store. In our case, we do this with MessageStore.newMessage
  3. The data store sends an event, which we subscribe to in componentWillMount
  4. Components which display data from the store listen to it, and update their internal state, as we do within updateMessages

Conclusion

React makes use of one way data binding to make it easier to reason about our code. Data always flows from parent to child, so you always know where something came from. For updating data, we pass in functions in props, so we can keep our state centralized within a controller view.

Although we can keep state within our components, some of it is better kept outside. We don’t need to use a complex Flux-type architecture – a simple object with some event listeners suffices just fine for many uses. The benefits from separating this includes better modularity, easier code reuse and easier testability.

The source code up to this point is available on GitHub.

What’s next? Now that we have a better data model, we can start adding support for multiple chat participants. This means we need some way of connecting chatters with each other, and I think WebRTC sounds suitably interesting for this task!

In the next article in the series we’ll finally make it possible for people to talk to each other!

If you don’t want to miss any follow-up articles in this series, sign up for my newsletter using the form below.