WebSockets with React, Redux, and Ruby on Rails
Tags: react, rails, redux, webdev
Published: 2022-01-08 22:32:20

**Note**: all the code for this post can be found [here(frontend)](https://github.com/fmendez/chat-app-ui) and [here (backend/rails)](https://github.com/fmendez/chat-app-backend-rails)

Let's explore how to integrate Rails ActionCable functionality (WebSockets) with a basic chat application using React and Redux (via Redux Toolkit). I'm only including the most relevant snippets of code, please refer to the code in the [repo](https://github.com/fmendez/chat-app-ui) for the entire context.

This is what we will be building:

![demo gif](https://fmendezblogbuckets3.s3.us-east-2.amazonaws.com/websocket-with-react-redux-rails/chatappdemo.gif)

## Backend

Since I'm using rails as an API endpoint, I'll create the app using the `--api` flag. This will prevent views from being generated when we call any of the `rails generate` commands, hence avoiding unnecessary code. Additionally, we'll use `postgresql` as the DB.

```js
rails new chat-app-backend-rails --api -database=postgresql

```

Since we're building our frontend as a separate standalone project, potentially deployed on a different server than our API, we need to allow for cross domain calls. For that, we first add `rack-cors` on the `Gemfile`:

```js
gem 'rack-cors'
```

And then configure it on `config/initializers/cors.rb`.

```ruby
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    # In a prod app you'll restrict to specific origin(s).
    # for this will just allow from any.
    origins '*'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

```

We then `bundle install` to install the gem we added.

Our app will simply have User and Messages. Let's create the models for that:

```ruby
rails generate model User
rails generate model Message
```

Our `User` will only have `username` and `status` this is what the migration looks like:

```ruby
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :username
      t.string :status
      t.timestamps
    end
  end
end
```

And for the `Message`:

```ruby
class CreateMessages < ActiveRecord::Migration[7.0]
  def change
    create_table :messages do |t|
      t.string :content
      t.timestamps
    end
  end
end

```

Our models have a `1-to-many` relationship (1 user has many messages). We'll capture that by adding `has_many :messages` on the `User` and `belongs_to` on `Message`.

```ruby
class User < ApplicationRecord
  has_many :messages, dependent: :destroy
end

class Message < ApplicationRecord
  belongs_to :user
end
```

Lastly, we'll add a migration that adds the reference (`user_id`) to messages.

```ruby
rails generate migration AddBelongToMessages
```

With this code:

```ruby
class AddBelongToMessages < ActiveRecord::Migration[7.0]
  def change
    add_belongs_to :messages, :user
  end
end
```

**Note:** We could have added this when we first created the `Message` migration.

Finally, we run the `migrate` command:

`rails db:migrate`

Next, let's add all the routes we'll be using and mount the ActionCable (WebSocket) server:

```ruby
  resources :messages, only: %i[index]
  resources :users, only: %i[index create] do
    post 'add_message'
    post 'change_status'
  end
  mount ActionCable.server => '/cable'
```

That's it for the setup. We're now ready to start adding some functionality. Let's start creating the `messages` and `users` channels. We'll use these to listen for messages posted on the chat and for users joining.

```ruby
rails generate channel messages
rails generate channel users
```

In both generated channels, we'll simply change the `subscribed` method to specify where we're streaming from:

```ruby
class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'message_channel'
  end

  def unsubscribed; end
end

class UsersChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'user_channel'
  end

  def unsubscribed; end
end
```

Now we can use the `ActionCable.server.broadcast()` method to broadcast to all the subscribers on those channels. We want to notify to all subscribers of the `user_channel` when a user joins the chat. We also want to notify the `message_channel` after sending messages. Let's do both of those things on the `UsersController`:

```ruby
class UsersController < ApplicationController
  def index
    users = User.all
    render json: users
  end

  def create
    user = User.new(user_params)
    ActionCable.server.broadcast('user_channel', user) if user.save
    render json: user
  end

  def add_message
    user = User.find(params[:user_id])
    message = params[:message]
    created_message = user.messages.create(content: message)
    ActionCable.server.broadcast('message_channel', created_message) if user.save
    head :ok
  end

  def change_status; end

  def user_params
    params.require(:user).permit(:username, :status)
  end
end
```

For completion, we also have our `MessagesController` that returns all messages for the users who just joined the chat (that way they can see what was said before them joining).

```ruby
class MessagesController < ApplicationController
  def index
    messages = Message.all
    render json: messages
  end
end
```

With that, we have all the API calls we need to integrate with our frontend:

```ruby
rails routes | grep users

user_add_message POST   /users/:user_id/add_message(.:format)

user_change_status POST   /users/:user_id/change_status(.:format)

users GET    /users(.:format)

POST   /users(.:format)                                                                                  users#create



rails routes | grep messages

messages GET    /messages(.:format)


```

## Frontend

For the frontend, I'll be using react with redux and typescript. Let's create the app:

```ruby
npx create-react-app chat-app-ui --template redux-typescript
```

This template will give you an application skeleton that uses redux with toolkit already setup (e.g., a sample reducer, a configured store, etc.).

I'll start by creating a `/features/users` folder. In there I'll add all the `api` and `reducer` functionality. In there I created a `usersAPI` with all the backend calls related to users. For example, this is how we're adding a new user to the chat:

```js
export const addNewUser = async (user: UserType): Promise<any> => {
  const res = await fetch("http://localhost:3090/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(user),
  });

  return await res.json();
};
```

And this is how we handle a user sending a message:

```js
export const sendUserMessage = async (
  data: sendUserMessageDataType
): Promise<any> => {
  const res = await fetch(
    `http://localhost:3090/users/${data.user.id}/add_message`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        user_id: data.user.id,
        message: data.message.content,
      }),
    }
  );

  return await res.json();
};
```

We will use these API calls indirectly via Redux thunks.

When working with async calls in the frontend, we usually make the async call and if it succeeds, we update the application state (e.g., Redux state) with the results. With thunks, the process is the same, but all is handled in the reducer itself. We only have to dispatch an action and after is fulfilled (e.g., call succeeded) then we update the state.

This is what a thunk looks like for adding a new user and for sending messages:

```js
...
export const addUserAsync = createAsyncThunk(
  'users/addUser',
  async (user: UserType) => {
    const response = await addNewUser(user);
    return response;
  }
)

export const sendMessageAsync = createAsyncThunk(
  'users/sendMessage',
  async (data: sendUserMessageDataType) => {
    const response = await sendUserMessage(data);
    return response;
  }
)
...
```

We then configure them on the `extraReducers` section of the `createSlice()`.

```js
...
  extraReducers: (builder) => {
    builder
      .addCase(sendMessageAsync.fulfilled, (state, action) => {
        let updatedUser: UserType = state.value.filter(user => user.id === action.payload.user.id)[0];
        updatedUser.messages.push(action.payload.message);
        state.value = state.value.map(user => user.id !== updatedUser.id ? user : updatedUser)
      })

      .addCase(addUserAsync.fulfilled, (state, action) => {
        state.value.push(action.payload);
        localStorage.setItem("currentUser", JSON.stringify(action.payload));
        state.userLoggedIn = true;
      })
  },
...
```

You can review the entire reducer [here](https://github.com/fmendez/chat-app-ui/blob/main/src/features/users/usersSlice.ts).

To call Rails's `ActionCable` we have to install the `actioncable` package.

```js
npm install --save actioncable

```

This is how we're using `actioncable` in the `Messages.tsx` to subscribe to new messages posted:

```js
import { useAppDispatch, useAppSelector } from "../app/hooks";
import { addMessage, selectMessages } from "../features/messages/messagesSlice";
import { MessageType } from "../types";
import Message from "./Message";
import ActionCable from "actioncable";
import { useEffect } from "react";

function Messages() {
  const messages: MessageType[] = useAppSelector(selectMessages);
  const cable = ActionCable.createConsumer("ws://localhost:3090/cable");
  const dispatch = useAppDispatch();

  const createSubscription = () => {
    cable.subscriptions.create(
      { channel: "MessagesChannel" },
      { received: (message) => handleReceivedMessage(message) }
    );
  };

  const handleReceivedMessage = (message: any) => {
    dispatch(addMessage(message));
  };

  useEffect(() => {
    createSubscription();
  }, []);

  return (
    <div className="">
      {messages.map((message) => (
        <Message key={message.id} message={message} />
      ))}
    </div>
  );
}

export default Messages;
```

We use the same approach on the [Users.tsx](https://github.com/fmendez/chat-app-ui/blob/main/src/components/Users.tsx#L1) to subscribe to new users joining the chat.

With everything configured and styled, this is what the entire chat application looks like:


![demo gif](https://fmendezblogbuckets3.s3.us-east-2.amazonaws.com/websocket-with-react-redux-rails/chatappdemo.gif)

With that, we have an app using WebSockets with React, Redux, and Rails.