Code Monkey home page Code Monkey logo

kesha-antonov / react-native-action-cable Goto Github PK

View Code? Open in Web Editor NEW

This project forked from schneidmaster/action-cable-react

58.0 6.0 24.0 364 KB

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

Home Page: https://www.npmjs.com/package/@kesha-antonov/react-native-action-cable

License: MIT License

CoffeeScript 91.48% JavaScript 8.52%
react-native reactnative rails actioncable websockets react action-cable rails5 rails6 realtime realtime-messaging realtime-updates ruby-on-rails rubyonrails

react-native-action-cable's Introduction

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

react-native-action-cable's People

Contributors

buk1m avatar cprodhomme avatar croaker avatar kesha-antonov avatar meenie avatar schneidmaster avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

react-native-action-cable's Issues

removeEventListener is deprecated

On the newer versions of React Native, the removeEventListener is deprecated.

https://reactnative.dev/docs/appstate#removeeventlistener

In connection_monitor:

AppState.removeEventListener("change", @visibilityDidChange)

I think it needs to be something like this (I've never done coffeescript before):

  start: =>
    unless @isRunning()
      @startedAt = now()
      delete @stoppedAt
      @startPolling()
      @eventListener = AppState.addEventListener("change", @visibilityDidChange)
      @log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")

  stop: =>
    if @isRunning()
      @stoppedAt = now()
      @stopPolling()
      if @eventListener
        @eventListener.remove()
      @log("ConnectionMonitor stopped")

Possible to listen to ActionCable events

So I know we can set up listeners for events like received, connected, rejected. But can you set up a listener when an error occurs while trying to connect to the WebSocket? For instance if we don't have internet access or the host is wrong?

App is connecting to action cable multiple times

ActionCableConnector.js


import { ActionCable, Cable } from '@kesha-antonov/react-native-action-cable';
import { WEB_SOCKET_URL } from '../constants/url';

const connectActionCable = ActionCable.createConsumer(WEB_SOCKET_URL);

const cable = new Cable({});

import { getPubSubToken } from './AuthHelper';

import {
  addConversation,
  addMessageToConversation,
} from '../actions/conversation';

import { store } from '../store';

class ActionCableConnector {
  constructor(pubSubToken) {
    const channel = cable.setChannel(
      'RoomChannel',
      connectActionCable.subscriptions.create({
        channel: 'RoomChannel',
        pubsub_token: pubSubToken,
      }),
    );

    channel.on('received', this.onReceived);

    this.events = {
      'messageCreated': this.onMessageCreated,
    };
  }

  onReceived = ({ event, data } = {}) => {
  
    if (this.events[event] && typeof this.events[event] === 'function') {
      this.events[event](data);
    }
  };

  onMessageCreated = message => {
    store.dispatch(addMessageToConversation({ message }));
  };


}

export async function initActionCable() {
  const pubSubToken = await getPubSubToken();

  const actionCable = new ActionCableConnector(pubSubToken);
  return actionCable;
}

Root Component

import React, { Component } from 'react';
import { initActionCable } from '../../helpers/ActionCable';

class ConversationList extends Component {

  componentDidMount = () => {
    initActionCable();
  };
  render = () => {
    return (
      <Layout>
      </Layout>
    );
  };
}

export default ConversationList;

The connection to action cable is working here. But the problem is that whenever I tried to restart the app, It got connected to action cable again. So multiple events are firing. Is there any way to check the connection is already established or not?

Not receiving data!

Hello! @kesha-antonov thank you for the package, but is not receiving or something that i did wrong?

here is the configuration:

createSocket() {
   let actionCable = ActionCable.createConsumer("ws://localhost:3000/cable");

   this.chats = actionCable.subscriptions.create(
     {
       channel: "NotificationChannel"
     },
     {
       connected: () => {},
       received: data => {
   
         this.fetchNewNotification(); 
       }
     }
   );
 }

Unable to use the package

After installing i followed the instruction and var actionCable = ActionCable.createConsumer('ws://localhost:3000/cable'); this line fails.

screenshot_2019-01-29-16-32-08-061_com gopher

How to properly mock @kesha-antonov/react-native-action-cable for Jest tests?

'm currently working on a project that utilizes @kesha-antonov/react-native-action-cable for handling WebSocket connections in a React Native application. In order to effectively test my components that use this library, I need to mock it properly within my Jest tests.

I have attempted to mock @kesha-antonov/react-native-action-cable using the following approach:

jest.mock('@kesha-antonov/react-native-action-cable', () => ({
  ActionCable: jest.fn().mockImplementation(() => ({
    createConsumer: jest.fn().mockImplementation(() => ({})),
  })),
  Cable: jest.fn().mockImplementation(() => {}),
}));

However, despite this mock implementation, I'm encountering difficulties in properly mocking the functionality of ActionCable and createConsumer within my tests.

Could someone please provide guidance on how to correctly mock @kesha-antonov/react-native-action-cable for Jest tests? Additionally, any examples or additional resources would be greatly appreciated.

Thank you in advance for your assistance!

Publish this?

This looks really promising, but it doesn't seem to be published?

ยป yarn add react-native-action-cable                                                                                          
yarn add v1.10.1
[1/4] ๐Ÿ”  Resolving packages...
error An unexpected error occurred: "https://registry.yarnpkg.com/react-native-action-cable: Not found".

clarification on example in the README

Thank you for this library! I have a question about the example in the README. How are you passing the the chatID and userID from the Rails app when setting the channel name in chat_${chatId}_${userId}?

Crash when data in received() is null

Hello! First, thanks for this fork, it's nice!

My problem:

TypeError: null is not an object (evaluating 't.action')

e#received
    subscription.js:1:1105
received
    [native code]:0
notify
    index.ios.bundle?platform=ios&dev=true&minify=false:297021:73
notify
    [native code]:0
t.prototype.events.message
    connection.js:1:3239
message
    [native code]:0
EventTarget.prototype.dispatchEvent
    event-target-shim.js:818:39
_eventEmitter.addListener$argument_1
    WebSocket.js:232:27
emit
    EventEmitter.js:190:12
callFunctionReturnFlushedQueue
    [native code]:0

It happen when the data of received() is null. Is it normal? I think it shouldn't crash, we don't necessary need any data for simple features use.

Crash is located in library's subscription.js file, in this function:

received (t) {
    return boundMethodCheck(this,e),t.action=null!=t.action?t.action:"received",this.emit(t.action,t)
  }};

Example Implementation for React-Native/Rails/GraphQl/Apollo

We did not find any solutions for our stack React-Native/Rails/GraphQl/Apollo and specifically had problems creating an ActionCable that connected properly with GraphQL subscriptions. The solution was a combination of this very nice package (thanks @kesha-antonov!!) and adjusting graphql-ruby ActionCableLink to work with the interface of react-native-action-cable. In case anyone else is having trouble with this stack I thought it might be nice to see a working example. Maybe in the README or example folder etc.

index.js

/**
 * @format
 */

import React from 'react';
import { ActionCable, Cable } from '@kesha-antonov/react-native-action-cable';
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { AppRegistry } from 'react-native';
import { createHttpLink } from 'apollo-link-http';
import { getMainDefinition } from 'apollo-utilities';
import { InMemoryCache } from 'apollo-cache-inmemory';

import AppWithNavigator from './src/navigators';
import { name as appName } from './app.json';
import ActionCableLink from './src/helpers/ActionCableLink';

const httpLink = createHttpLink({ uri: 'http://localhost:3000/graphql' });
const actionCable = ActionCable.createConsumer('ws://localhost:3000/cable');
const cable = new Cable({});

const hasSubscriptionOperation = ({ query }) => {
  const { kind, operation } = getMainDefinition(query);

  return kind === 'OperationDefinition' && operation === 'subscription';
};

const link = ApolloLink.split(
  hasSubscriptionOperation,
  new ActionCableLink({ actionCable, cable }),
  httpLink
);

const client = new ApolloClient({
  link,
  cache: new InMemoryCache()
});

const AppWithApollo = () => (
  <ApolloProvider client={client}>
    <AppWithNavigator />
  </ApolloProvider>
);

AppRegistry.registerComponent(appName, () => AppWithApollo);

adjusted ActionCableLink.js

import { ApolloLink, Observable } from 'apollo-link';

const printer = require('graphql/language/printer');

function ActionCableLink(options) {
  const { cable, actionCable } = options;
  const { connectionParams = {} } = options;
  const channelName = options.channelName || 'GraphqlChannel';
  const actionName = options.actionName || 'execute';

  return new ApolloLink(operation => (
    new Observable((observer) => {
      const channelId = Math.round(
        Date.now() + Math.random() * 100000
      ).toString(16);

      const channel = cable.setChannel(
        'GraphqlChannel', // channel name to which we will pass data from Rails app with `stream_from`
        actionCable.subscriptions.create({
          channel: channelName,
          channelId,
          ...connectionParams
        })
      );

      /* eslint-disable func-names */
      channel.on('connected', function () {
        this.perform(
          actionName,
          {
            query: operation.query ? printer.print(operation.query) : null,
            variables: operation.variables,
            operationId: operation.operationId,
            operationName: operation.operationName
          }
        );
      }).on('received', function (payload) {
        if (payload.result.data || payload.result.errors) {
          observer.next(payload.result);
        }

        if (!payload.more) {
          this.unsubscribe();
          observer.complete();
        }
      });

      /* eslint-enable func-names */

      return channel;
    })
  ));
}

module.exports = ActionCableLink;

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.