Building Real-Time Features with Rails ActionCable and React Native
[ Mobile Development ]

Building Real-Time Features with Rails ActionCable and React Native

Build real-time chat and live notifications with Rails ActionCable and React Native. Master WebSocket connections, message broadcasting, and client synchronization.

→ featured
→ essential
→ timely
By Paul Badarau 11 min read
[ Article Content ]
Share this article:
P
Paul Badarau
Author

Building Real-Time Features with Rails ActionCable and React Native

Primary Keyword: Rails ActionCable React Native
Meta Description: Build real-time chat and live notifications with Rails ActionCable and React Native. Master WebSocket connections, message broadcasting, and client synchronization.


The Real-Time Expectation

Your user sends a message. They expect to see it instantly. They expect the recipient to see it instantly too.

You poll the server every 5 seconds. Messages arrive with a delay. Users think your app is broken. They leave.

This is the real-time problem. HTTP isn't built for it. You need WebSockets—persistent connections that push updates immediately.

Rails has ActionCable. React Native has WebSocket support. Together, they build real-time features that feel native.

Why this matters: We've shipped real-time features—chat, notifications, live dashboards—in production apps. The difference between polling (checking the server repeatedly) and WebSockets (server pushes updates) is night and day. Real-time isn't a luxury; it's table stakes for modern apps.

By the end of this guide, you'll understand:

  • How WebSockets differ from HTTP (and why they matter).
  • How to set up ActionCable in Rails for broadcasting messages.
  • How to connect a React Native client to ActionCable channels.
  • How to build a chat feature and live notifications.
  • How to handle reconnections, errors, and offline states.

WebSockets vs. HTTP: The Fundamental Difference

HTTP: Request-Response

HTTP is stateless. The client asks, the server responds, the connection closes.

Client: "Give me the latest messages"
Server: "Here are 10 messages"
[Connection closes]

[5 seconds later]
Client: "Give me the latest messages again"
Server: "Here are 11 messages (1 new)"
[Connection closes]

This works for static content. It's terrible for real-time updates.

Problems with polling:

  • Latency: Updates arrive every 5–10 seconds (the polling interval).
  • Wasted requests: 90% of polls return "no new data."
  • Server load: Every poll is a new HTTP request.

WebSockets: Persistent Connection

WebSockets keep the connection open. The server pushes updates when they happen.

Client: [Opens WebSocket connection]
Server: [Accepts connection]

[10 seconds later, a new message arrives]
Server: "New message: 'Hello!'"
Client: [Receives and displays immediately]

[Connection stays open]

Advantages:

  • Instant updates: No polling delay.
  • Efficient: No wasted requests.
  • Bidirectional: Client and server can both send messages.

ActionCable: Rails' Real-Time Solution

ActionCable is Rails' built-in WebSocket framework. It integrates with Rails' existing infrastructure (ActiveRecord, authentication, etc.).

How ActionCable Works

  1. Channels: A channel is a logical stream (e.g., ChatChannel, NotificationChannel).
  2. Broadcasting: The server broadcasts messages to a channel.
  3. Subscriptions: Clients subscribe to channels and receive broadcasts.

Setting Up ActionCable in Rails

1. Generate a channel:

rails generate channel Chat

This creates:

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    # Called when the client subscribes
    stream_from "chat_channel"
  end

  def unsubscribed
    # Called when the client disconnects
  end

  def receive(data)
    # Called when the client sends a message
    ActionCable.server.broadcast("chat_channel", data)
  end
end

2. Configure ActionCable:

In config/cable.yml:

development:
  adapter: async

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: myapp_production

For production, use Redis. ActionCable needs a pub/sub backend to broadcast across multiple servers. Redis is the standard.

3. Authenticate the connection:

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

    def find_verified_user
      token = request.params[:token]
      payload = JwtToken.decode(token)
      if payload && (user = User.find_by(id: payload['user_id']))
        user
      else
        reject_unauthorized_connection
      end
    end
  end
end

This authenticates the WebSocket connection using a JWT token.


Building a Chat Feature

Let's build a simple chat room: users send messages, all connected users see them instantly.

Rails Backend

1. Create a Message model:

rails generate model Message user:references content:text
rails db:migrate

2. Create a Messages controller:

# app/controllers/api/v1/messages_controller.rb
class Api::V1::MessagesController < Api::V1::BaseController
  def index
    messages = Message.order(created_at: :desc).limit(50)
    render json: messages.as_json(include: :user)
  end

  def create
    message = current_user.messages.build(message_params)
    if message.save
      # Broadcast to all subscribers
      ActionCable.server.broadcast(
        "chat_channel",
        {
          id: message.id,
          content: message.content,
          user: {
            id: current_user.id,
            name: current_user.name
          },
          created_at: message.created_at
        }
      )
      render json: message, status: :created
    else
      render_error('Failed to send message', :unprocessable_entity)
    end
  end

  private

  def message_params
    params.require(:message).permit(:content)
  end
end

3. Update the ChatChannel:

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_channel"
  end

  def unsubscribed
    # Cleanup when disconnected
  end
end

React Native Client

1. Install ActionCable client:

npm install @rails/actioncable

2. Create a WebSocket utility:

// lib/cable.ts
import ActionCable from '@rails/actioncable';
import { getToken } from './auth';

let cable: ActionCable.Cable | null = null;

export async function getCable() {
  if (!cable) {
    const token = await getToken();
    cable = ActionCable.createConsumer(
      `ws://localhost:3000/cable?token=${token}`
    );
  }
  return cable;
}

export function disconnectCable() {
  if (cable) {
    cable.disconnect();
    cable = null;
  }
}

3. Create a Chat screen:

// screens/ChatScreen.tsx
import { useEffect, useState, useRef } from 'react';
import { View, FlatList, TextInput, Button, Text } from 'react-native';
import { getCable } from '@/lib/cable';
import apiClient from '@/lib/api';

interface Message {
  id: number;
  content: string;
  user: {
    id: number;
    name: string;
  };
  created_at: string;
}

export default function ChatScreen() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputText, setInputText] = useState('');
  const channelRef = useRef<any>(null);

  useEffect(() => {
    // Load initial messages
    loadMessages();

    // Subscribe to chat channel
    subscribeToChat();

    return () => {
      // Unsubscribe when component unmounts
      if (channelRef.current) {
        channelRef.current.unsubscribe();
      }
    };
  }, []);

  const loadMessages = async () => {
    try {
      const response = await apiClient.get('/messages');
      setMessages(response.data.reverse());  // Show oldest first
    } catch (error) {
      console.error('Failed to load messages', error);
    }
  };

  const subscribeToChat = async () => {
    const cable = await getCable();
    
    channelRef.current = cable.subscriptions.create('ChatChannel', {
      received(data: Message) {
        // New message received
        setMessages((prev) => [...prev, data]);
      },
      connected() {
        console.log('Connected to chat');
      },
      disconnected() {
        console.log('Disconnected from chat');
      },
    });
  };

  const sendMessage = async () => {
    if (!inputText.trim()) return;

    try {
      await apiClient.post('/messages', {
        message: { content: inputText },
      });
      setInputText('');
    } catch (error) {
      console.error('Failed to send message', error);
    }
  };

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <FlatList
        data={messages}
        renderItem={({ item }) => (
          <View style={{ marginBottom: 10 }}>
            <Text style={{ fontWeight: 'bold' }}>{item.user.name}</Text>
            <Text>{item.content}</Text>
            <Text style={{ fontSize: 10, color: '#888' }}>
              {new Date(item.created_at).toLocaleTimeString()}
            </Text>
          </View>
        )}
        keyExtractor={(item) => item.id.toString()}
      />
      
      <View style={{ flexDirection: 'row', marginTop: 10 }}>
        <TextInput
          style={{ flex: 1, borderWidth: 1, padding: 10 }}
          value={inputText}
          onChangeText={setInputText}
          placeholder="Type a message..."
        />
        <Button title="Send" onPress={sendMessage} />
      </View>
    </View>
  );
}

How It Works

  1. User opens the chat screen:

    • Loads the last 50 messages via HTTP.
    • Subscribes to the ChatChannel via WebSocket.
  2. User sends a message:

    • Sends a POST request to /messages.
    • Rails saves the message and broadcasts it to all subscribers.
  3. All connected clients receive the message:

    • ActionCable pushes the message through the WebSocket.
    • The received() callback adds it to the local state.
    • The UI updates instantly.

Building Live Notifications

Notifications are simpler: the server pushes, the client displays.

Rails Backend

1. Create a NotificationChannel:

# app/channels/notification_channel.rb
class NotificationChannel < ApplicationCable::Channel
  def subscribed
    # Subscribe to user-specific notifications
    stream_for current_user
  end
end

2. Broadcast a notification:

# Anywhere in your Rails code
NotificationChannel.broadcast_to(
  user,
  {
    title: "New message",
    body: "You have a new message from Alice",
    data: { message_id: 123 }
  }
)

React Native Client

1. Subscribe to notifications:

// hooks/useNotifications.ts
import { useEffect, useState } from 'react';
import { getCable } from '@/lib/cable';

interface Notification {
  title: string;
  body: string;
  data?: any;
}

export function useNotifications() {
  const [notifications, setNotifications] = useState<Notification[]>([]);

  useEffect(() => {
    let channel: any;

    const subscribe = async () => {
      const cable = await getCable();
      channel = cable.subscriptions.create('NotificationChannel', {
        received(data: Notification) {
          setNotifications((prev) => [...prev, data]);
          // Optionally show a local notification
          showLocalNotification(data);
        },
      });
    };

    subscribe();

    return () => {
      if (channel) channel.unsubscribe();
    };
  }, []);

  return notifications;
}

function showLocalNotification(notification: Notification) {
  // Use expo-notifications or react-native-push-notification
  console.log('Notification:', notification.title);
}

2. Use in a component:

// screens/HomeScreen.tsx
import { useNotifications } from '@/hooks/useNotifications';

export default function HomeScreen() {
  const notifications = useNotifications();

  return (
    <View>
      <Text>Notifications: {notifications.length}</Text>
      {notifications.map((notif, i) => (
        <Text key={i}>{notif.title}: {notif.body}</Text>
      ))}
    </View>
  );
}

Handling Reconnections and Errors

WebSockets can disconnect. Networks fail. Your app needs to handle this.

Automatic Reconnection

ActionCable handles reconnection automatically. But you should show the user when they're disconnected.

const subscribeToChat = async () => {
  const cable = await getCable();
  
  channelRef.current = cable.subscriptions.create('ChatChannel', {
    received(data: Message) {
      setMessages((prev) => [...prev, data]);
    },
    connected() {
      console.log('Connected to chat');
      setConnectionStatus('connected');
    },
    disconnected() {
      console.log('Disconnected from chat');
      setConnectionStatus('disconnected');
    },
  });
};

Display the status:

{connectionStatus === 'disconnected' && (
  <Text style={{ color: 'red' }}>Reconnecting...</Text>
)}

Queuing Messages While Offline

If the connection is lost, queue messages locally and send them when reconnected.

const sendMessage = async () => {
  if (connectionStatus === 'disconnected') {
    // Queue message locally
    queueMessage(inputText);
    setInputText('');
    return;
  }

  try {
    await apiClient.post('/messages', {
      message: { content: inputText },
    });
    setInputText('');
  } catch (error) {
    // Queue if request fails
    queueMessage(inputText);
  }
};

// When reconnected, send queued messages
useEffect(() => {
  if (connectionStatus === 'connected') {
    sendQueuedMessages();
  }
}, [connectionStatus]);

Scaling ActionCable

ActionCable scales horizontally with Redis. Each Rails server broadcasts to Redis, and Redis pushes to all connected clients.

Production Setup

1. Use Redis:

# config/cable.yml
production:
  adapter: redis
  url: <%= ENV['REDIS_URL'] %>

2. Deploy multiple Rails servers:

Each server handles WebSocket connections. Redis coordinates broadcasts.

3. Monitor connections:

ActionCable exposes metrics. Use tools like Scout or DataDog to monitor:

  • Number of active connections.
  • Messages per second.
  • Redis pub/sub latency.

Common Pitfalls

1. Not Authenticating WebSocket Connections

Always authenticate the WebSocket. Otherwise, anyone can connect and listen to broadcasts.

2. Broadcasting Sensitive Data

Don't broadcast passwords, tokens, or private user data. Only broadcast what the client needs.

3. Not Handling Reconnections

Networks fail. Test your app with airplane mode toggled on/off. Does it reconnect? Does it queue messages?

4. Forgetting to Unsubscribe

If a component unmounts, unsubscribe from the channel. Otherwise, you'll have memory leaks.

return () => {
  if (channelRef.current) {
    channelRef.current.unsubscribe();
  }
};

Bringing It All Together

Real-time features—chat, notifications, live updates—aren't optional anymore. Users expect them.

Rails ActionCable + React Native makes this straightforward:

  1. Rails broadcasts messages through ActionCable channels.
  2. React Native subscribes to those channels via WebSocket.
  3. Updates push instantly to all connected clients.

The pattern works for chat, notifications, live dashboards, collaborative editing, and more.

Start with a simple chat. Add notifications. Then experiment with live data (stock prices, game scores, order status).

Real-time isn't magic. It's WebSockets, Redis, and good state management. You already know enough to build it.

Have you built real-time features? What worked? What broke? Share on Twitter or LinkedIn—we'd love to hear your experience.


Further Reading

[ Let's Build Together ]

Ready to transform your
business with software?

From strategy to implementation, we craft digital products that drive real business outcomes. Let's discuss your vision.

Related Topics:
Rails ActionCable React Native WebSockets real-time 2025