DEV Community

Cover image for Web Sockets with Rails 6 and ReactJS
Brandon Brown
Brandon Brown

Posted on

Web Sockets with Rails 6 and ReactJS

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.

gem 'rack-cors'

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

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.

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:

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:

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.

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

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!

Top comments (0)