Use Rails 5+ ActionCable channels with React Native for realtime magic.
This is a fork from https://github.com/schneidmaster/action-cable-react
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.
yarn add @kesha-antonov/react-native-action-cable
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')
ActionCable
top level methods:
.createConsumer(websocketUrl, headers = {})
- create actionCable consumer and start connecting.websocketUrl
- url to your Rails app'scable
endpointheaders
- headers to send with connection request
.startDebugging()
- start logging.stopDebugging()
- stop logging
ActionCable
instance methods:
.open()
- try connect.connection.isOpen()
- check ifconnected
.connection.isActive()
- check ifconnected
orconnecting
.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 bereceived
,connected
,rejected
,disconnected
or value ofdata.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'
- Fork it ( https://github.com/kesha-antonov/react-native-action-cable/fork )
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new Pull Request
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.
MIT