Skip to content

kesha-antonov/react-native-action-cable

 
 

Repository files navigation

npm version Bower version

ActionCable + React Native

Use Rails 5+ ActionCable channels with React Native for realtime magic.

This is a fork from https://github.com/schneidmaster/action-cable-react

Overview

The react-native-action-cable package exposes two modules: ActionCable, Cable.

  • ActionCable: holds info and logic of connection and automatically tries to reconnect when connection is lost.
  • Cable: holds references to channels(subscriptions) created by action cable.

Install

yarn add @kesha-antonov/react-native-action-cable

Usage

Import:

import {
  ActionCable,
  Cable,
} from '@kesha-antonov/react-native-action-cable'

Define once ActionCable and Cable in your application setup in your store (like Redux or MobX).

Create your consumer:

const actionCable = ActionCable.createConsumer('ws://localhost:3000/cable')

Right after that create Cable instance. It'll hold info of our channels.

const cable = new Cable({})

Then, you can subscribe to channel:

const channel = cable.setChannel(
  `chat_${chatId}_${userId}`, // channel name to which we will pass data from Rails app with `stream_from`
  actionCable.subscriptions.create({
    channel: 'ChatChannel', // from Rails app app/channels/chat_channel.rb
    chatId,
    otherParams...
  })
)

channel
  .on( 'received', this.handleReceived )
  .on( 'connected', this.handleConnected )
  .on( 'rejected', this.handleDisconnected )
  .on( 'disconnected', this.handleDisconnected )

...later we can remove event listeners and unsubscribe from channel:

const channelName = `chat_${chatId}_${userId}`
const channel = cable.channel(channelName)
if (channel) {
  channel
    .removeListener( 'received', this.handleReceived )
    .removeListener( 'connected', this.handleConnected )
    .removeListener( 'rejected', this.handleDisconnected )
    .removeListener( 'disconnected', this.handleDisconnected )
  channel.unsubscribe()
  delete( cable.channels[channelName] )
}

You can combine React's lifecycle hook useEffect to subscribe and unsubscribe from channels. Or implement custom logic in your store.

Here's example how you can handle events:

function Chat ({ chatId, userId }) {
  const [isWebsocketConnected, setIsWebsocketConnected] = useState(false)

  const onNewMessage = useCallback(message => {
    // ... ADD TO MESSAGES LIST
  }, [])

  const handleReceived = useCallback(({ type, message }) => {
    switch(type) {
      'new_incoming_message': {
         onNewMessage(message)
      }
      ...
    }
  }, [])

  const handleConnected = useCallback(() => {
    setIsWebsocketConnected(true)
  }, [])

  const handleDisconnected = useCallback(() => {
    setIsWebsocketConnected(false)
  }, [])

  const getChannelName = useCallback(() => {
    return `chat_${chatId}_${userId}`
  }, [chatId, userId])

  const createChannel = useCallback(() => {
    const channel = cable.setChannel(
      getChannelName(), // channel name to which we will pass data from Rails app with `stream_from`
      actionCable.subscriptions.create({
        channel: 'ChatChannel', // from Rails app app/channels/chat_channel.rb
        chatId,
        otherParams...
      })
    )

    channel
      .on( 'received', handleReceived )
      .on( 'connected', handleConnected )
      .on( 'disconnected', handleDisconnected )
  }, [])

  const removeChannel = useCallback(() => {
    const channelName = getChannelName()

    const channel = cable.channel(channelName)
    if (!channel)
      return

    channel
      .removeListener( 'received', handleReceived )
      .removeListener( 'connected', handleConnected )
      .removeListener( 'disconnected', handleDisconnected )
    channel.unsubscribe()
    delete( cable.channels[channelName] )
  }, [])

  useEffect(() => {
    createChannel()

    return () => {
      removeChannel()
    }
  }, [])

  return (
    <View>
      // ... RENDER CHAT HERE
    </View>
  )
}

Send message to Rails app:

cable.channel(channelName).perform('send_message', { text: 'Hey' })

cable.channel('NotificationsChannel').perform('appear')

Methods

ActionCable top level methods:

  • .createConsumer(websocketUrl, headers = {}) - create actionCable consumer and start connecting.
    • websocketUrl - url to your Rails app's cable endpoint
    • headers - headers to send with connection request
  • .startDebugging() - start logging
  • .stopDebugging() - stop logging

ActionCable instance methods:

  • .open() - try connect
  • .connection.isOpen() - check if connected
  • .connection.isActive() - check if connected or connecting
  • .subscriptions.create({ channel, otherParams... }) - create subscription to Rails app
  • .disconnect() - disconnects from Rails app

Cable instance methods:

  • .setChannel(name, actionCable.subscriptions.create()) - set channel to get it later
  • .channel(name) - get channel by name

channel methods:

  • .perform(action, data) - send message to channel. action - string, data - json
  • .removeListener(eventName, eventListener) - unsubscribe from event
  • .unsubscribe() - unsubscribe from channel
  • .on(eventName, eventListener) - subscribe to events. eventName can be received, connected, rejected, disconnected or value of data.action attribute from channel message payload.

Custom action example:

{
  "identifier": "{\"channel\":\"ChatChannel\",\"id\":42}",
  "command": "message",
  "data": "{\"action\":\"speak\",\"text\":\"hello!\"}"
}

Above message will be emited with eventName = 'speak'

Contributing

  1. Fork it ( https://github.com/kesha-antonov/react-native-action-cable/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Credits

Obviously, this project is heavily indebted to the entire Rails team, and most of the code in lib/action_cable is taken directly from Rails 5. This project also referenced fluxxor for implementation details and props binding.

License

MIT