Web Sockets with Rails 6 and ReactJS
Tags: rails, react, webdev
Published: 2020-08-11 17:16:45

## Introduction

With social media platforms so prevalent nowadays in our every day lives, it is hard to go a day without utilizing a websocket. Websockets provide a connection between a backend and frontend server to allow for real-time data flow. The simplest, yet one of the most wide use cases of websockets is a simple message board. Here, I’m going to attempt to walk through a simple setup process for a message board with a Rails backend and a React frontend.

## Rails Setup

First start off by creating a new rails application:

```
rails new message-board-backend --api --database=postgresql
```

Here, we want to use rails as an API as to not clutter up our backend server with frontend items such as views. Additionally, we are using postgresql as our database because sqlite3 is not supported by Heroku in case one day we would like to deploy this app.

Once the app is created, we need to add the ability for the backend to receive requests from the frontend. This is handled through the rack-cors gem. To do this, we will need to uncomment the following line in the Gemfile.

```ruby
gem 'rack-cors'
```

We also need enable connections from our frontend. To do this, uncomment and update the following in *config/initializers/cors.rb*.

```ruby
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

Setting origins to “*” is alright for development, however, make sure to set this to the production URI before production. Now install the rack-cors gem with:

```
bundle install
```

Now we need to create our message model and then migrate. Run the following:

```
rails g model Message content --no-test-framework
rails db:migrate
```

Here, we are creating a message model with one column (content) with type string. Also we are using the no-test-framework to tell rails not to generate any test files.

Next, we are going to create our routes inside of *config/routes.rb*.

```ruby
Rails.application.routes.draw do
  resources :messages, only: [:index, :create]
  mount ActionCable.server => '/cable'
end
```

Here, we generate two routes for messages (index and create). We also generate a route to use as our websocket server endpoint. This is what we’ll use to “listen” for updates on the frontend.

In order for the route we just created to be functional, we need to create a channel for our messages. Channels provide the link for the websocket by broadcasting any new messages that are created. Create a channel in rails with:

```
rails g channel messages --no-test-framework
```

This creates a new file in app/channels called messages_channel.rb with methods subscribe and unsubscribe. Edit the file with the following:

```ruby
class MessagesChannel < ApplicationCabel:Channel
  def subscribed
    stream_from 'messages_channel'
  end
  def unsubscribed
  end
end
```

Next, we need to create our controller to handle returning all messages and the creation of new messages. Generate the controller with:

```
rails g controller messages --no-test-framework
```

Next we need to configure the new messages controller:

```ruby
class MessagesController < ApplicationController
  def index
    messages = Message.all
    render json: message
  end
  def create
    message = Message.new(message_params)
    if message.save
      ActionCable.server.broadcast 'messages_channel', message
      head :ok
    else
      head :ok
    end
  end
  private
  def message_params
    params.require(:message).permit(:content)
  end
end
```

If a valid message is sent, the message is created in the database, and ActionCable broadcasts the message to the messages_channel channel. If the message is not valid, writing “head :ok” will allow the server to continue the connection.

That does it for the backend. Now we can move on to the frontend.

## Frontend

First, start by creating a new react app by entering the following in the terminal.

```
yarn create react-app message-board-frontend
```

Next we will need to install react-actioncable-provider to connect to the rails ActionCable. Navigate to the newly created project and run the following in the terminal:

```
yarn add actioncable
```

Next we need to set up our index.js file. There really isn’t anything special here. We are really just calling the App component.

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

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

serviceWorker.unregister();
```

Now we need to update our App.js file to house our message board

```javascript
import React from 'react';
import { ActionCable } from 'actioncable';

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      messages: []
    }
    this.cable = ActionCable.createConsumer('ws://localhost:3000/cable')
  }

  componentDidMount() {
    this.fetch
    this.createSubscription()
  };

  fetchMessages = () => {
    fetch('http://localhost:3000/messages')
      .then(res => res.json())
      .then(messages => this.setState({ messages: messages });
  }

  createSubscription = () => {
    this.cable.subscriptions.create(
      { channel: 'MessagesChannel' },
      { received: message => this.handleReceivedMessage(message) }
    )
  }

  mapMessages = () => {
    return this.state.messages.map((message, i) => 
      <li key={i}>{message.content}</li>)
  }

  handleReceivedMessage = message => {
    this.setState({ messages: [...this.state.messages, message] })
  }

  handleMessageSubmit = e => {
    e.preventDefault();
    const messageObj = {
      message: {
        content: e.target.message.value
      }
    }
    const fetchObj = {
      method: 'POST'
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(messageObj)
    }
    fetch('http://localhost:3000/messages', fetchObj)
    e.target.reset()
  }

  render() {
    return (
      <div className='App'>
        <ActionCable 
          channel={{ channel: 'MessagesChannel' }}
          onReceived={this.handleReceivedMessages}
        />
        <h2>Messages</h2>
        <ul>{this.mapMessages()}</ul>
        <form>
          <input name='message' type='text' />
          <input type='submit' value='Send message' />
        </form>
      </div>
    );
  }
}
export default App;
```

The above code works as following: First, a consumer is established in the constructor. Then, all messages are fetched from the server and a subscription is created for the MessagesChannel channel. With this subscription, on any received data, the handleReceivedMessage function will execute which will append the new message to the component state. On an updated state, the component will re-render with the new message displayed.

## Conclusion

I hope this short blog post helps you set up your own websockets using Rails and ReactJS. Now you have the power to make astonishing applications with real-time updates!