{"id":14955431,"url":"https://github.com/kesha-antonov/react-native-action-cable","last_synced_at":"2025-09-29T20:31:27.036Z","repository":{"id":38715600,"uuid":"50319201","full_name":"kesha-antonov/react-native-action-cable","owner":"kesha-antonov","description":"Use Rails 5+ ActionCable channels with React Native for realtime magic.","archived":false,"fork":true,"pushed_at":"2024-03-02T20:11:26.000Z","size":364,"stargazers_count":57,"open_issues_count":7,"forks_count":25,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-01-13T00:59:13.255Z","etag":null,"topics":["action-cable","actioncable","rails","rails5","rails6","react","react-native","reactnative","realtime","realtime-messaging","realtime-updates","ruby-on-rails","rubyonrails","websockets"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@kesha-antonov/react-native-action-cable","language":"CoffeeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"schneidmaster/action-cable-react","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kesha-antonov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-01-25T02:01:39.000Z","updated_at":"2024-11-29T02:17:39.000Z","dependencies_parsed_at":"2023-01-21T22:47:58.526Z","dependency_job_id":null,"html_url":"https://github.com/kesha-antonov/react-native-action-cable","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kesha-antonov%2Freact-native-action-cable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kesha-antonov%2Freact-native-action-cable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kesha-antonov%2Freact-native-action-cable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kesha-antonov%2Freact-native-action-cable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kesha-antonov","download_url":"https://codeload.github.com/kesha-antonov/react-native-action-cable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234659895,"owners_count":18867636,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["action-cable","actioncable","rails","rails5","rails6","react","react-native","reactnative","realtime","realtime-messaging","realtime-updates","ruby-on-rails","rubyonrails","websockets"],"created_at":"2024-09-24T13:11:08.835Z","updated_at":"2025-09-29T20:31:27.030Z","avatar_url":"https://github.com/kesha-antonov.png","language":"CoffeeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm version](https://badge.fury.io/js/action-cable-react.svg)](https://badge.fury.io/js/action-cable-react)\n[![Bower version](https://badge.fury.io/bo/action-cable-react.svg)](https://badge.fury.io/bo/action-cable-react)\n\n# ActionCable + React Native\n\nUse Rails 5+ ActionCable channels with React Native for realtime magic.\n\nThis is a fork from https://github.com/schneidmaster/action-cable-react\n\n## Overview\n\nThe `react-native-action-cable` package exposes two modules: ActionCable, Cable.\n\n- **`ActionCable`**: holds info and logic of connection and automatically tries to reconnect when connection is lost.\n- **`Cable`**: holds references to channels(subscriptions) created by action cable.\n\n## Install\n\n```yarn add @kesha-antonov/react-native-action-cable```\n\n## React Native Compatibility\n\nThis library is fully compatible with React Native environments. It automatically detects the available WebSocket implementation and works without requiring `window` object polyfills.\n\n## Usage\n\nImport:\n\n```javascript\nimport {\n  ActionCable,\n  Cable,\n} from '@kesha-antonov/react-native-action-cable'\n```\n\nDefine once ActionCable and Cable in your application setup in your store (like `Redux` or `MobX`).\n\nCreate your consumer:\n\n```javascript\nconst actionCable = ActionCable.createConsumer('ws://localhost:3000/cable')\n```\n\nYou can also pass headers as a function for dynamic authentication:\n\n```javascript\n// Static headers\nconst actionCable = ActionCable.createConsumer('ws://localhost:3000/cable', {\n  'Authorization': 'Bearer token123'\n})\n\n// Dynamic headers (function that returns headers object)\nconst actionCable = ActionCable.createConsumer('ws://localhost:3000/cable', () =\u003e ({\n  'Authorization': `Bearer ${getCurrentAuthToken()}`\n}))\n```\n\nRight after that create Cable instance. It'll hold info of our channels.\n\n```javascript\nconst cable = new Cable({})\n```\n\nThen, you can subscribe to channel:\n\n```javascript\nconst channel = cable.setChannel(\n  `chat_${chatId}_${userId}`, // channel name to which we will pass data from Rails app with `stream_from`\n  actionCable.subscriptions.create({\n    channel: 'ChatChannel', // from Rails app app/channels/chat_channel.rb\n    chatId,\n    otherParams...\n  })\n)\n\nchannel\n  .on( 'received', this.handleReceived )\n  .on( 'connected', this.handleConnected )\n  .on( 'rejected', this.handleDisconnected )\n  .on( 'disconnected', this.handleDisconnected )\n  .on( 'error', this.handleError )\n```\n\n...later we can remove event listeners and unsubscribe from channel:\n\n```javascript\nconst channelName = `chat_${chatId}_${userId}`\nconst channel = cable.channel(channelName)\nif (channel) {\n  channel\n    .removeListener( 'received', this.handleReceived )\n    .removeListener( 'connected', this.handleConnected )\n    .removeListener( 'rejected', this.handleDisconnected )\n    .removeListener( 'disconnected', this.handleDisconnected )\n    .removeListener( 'error', this.handleError )\n  channel.unsubscribe()\n  delete( cable.channels[channelName] )\n}\n\n```\n\nYou can combine React's lifecycle hook `useEffect` to subscribe and unsubscribe from channels. Or implement custom logic in your `store`.\n\nHere's example how you can handle events:\n\n```javascript\nfunction Chat ({ chatId, userId }) {\n  const [isWebsocketConnected, setIsWebsocketConnected] = useState(false)\n\n  const onNewMessage = useCallback(message =\u003e {\n    // ... ADD TO MESSAGES LIST\n  }, [])\n\n  const handleReceived = useCallback(({ type, message }) =\u003e {\n    switch(type) {\n      'new_incoming_message': {\n         onNewMessage(message)\n      }\n      ...\n    }\n  }, [])\n\n  const handleConnected = useCallback(() =\u003e {\n    setIsWebsocketConnected(true)\n  }, [])\n\n  const handleDisconnected = useCallback(() =\u003e {\n    setIsWebsocketConnected(false)\n  }, [])\n\n  const handleError = useCallback((error) =\u003e {\n    console.log('WebSocket error:', error)\n    setIsWebsocketConnected(false)\n  }, [])\n\n  const getChannelName = useCallback(() =\u003e {\n    return `chat_${chatId}_${userId}`\n  }, [chatId, userId])\n\n  const createChannel = useCallback(() =\u003e {\n    const channel = cable.setChannel(\n      getChannelName(), // channel name to which we will pass data from Rails app with `stream_from`\n      actionCable.subscriptions.create({\n        channel: 'ChatChannel', // from Rails app app/channels/chat_channel.rb\n        chatId,\n        otherParams...\n      })\n    )\n\n    channel\n      .on( 'received', handleReceived )\n      .on( 'connected', handleConnected )\n      .on( 'disconnected', handleDisconnected )\n      .on( 'error', handleError )\n  }, [])\n\n  const removeChannel = useCallback(() =\u003e {\n    const channelName = getChannelName()\n\n    const channel = cable.channel(channelName)\n    if (!channel)\n      return\n\n    channel\n      .removeListener( 'received', handleReceived )\n      .removeListener( 'connected', handleConnected )\n      .removeListener( 'disconnected', handleDisconnected )\n      .removeListener( 'error', handleError )\n    channel.unsubscribe()\n    delete( cable.channels[channelName] )\n  }, [])\n\n  useEffect(() =\u003e {\n    createChannel()\n\n    return () =\u003e {\n      removeChannel()\n    }\n  }, [])\n\n  return (\n    \u003cView\u003e\n      // ... RENDER CHAT HERE\n    \u003c/View\u003e\n  )\n}\n\n```\n\nSend message to Rails app:\n\n```javascript\ncable.channel(channelName).perform('send_message', { text: 'Hey' })\n\ncable.channel('NotificationsChannel').perform('appear')\n```\n\n## Obtaining chatId and userId\n\nYou might wonder: \"Where do `chatId` and `userId` come from?\" These are values that your React Native app needs to obtain before creating the ActionCable subscription. Here are the most common approaches:\n\n### 1. From Navigation/Route Parameters\n```javascript\n// Using React Navigation\nfunction ChatScreen({ route }) {\n  const { chatId, userId } = route.params;\n  // Now use chatId and userId for your subscription\n}\n```\n\n### 2. From API Calls\n```javascript\nfunction Chat() {\n  const [chatId, setChatId] = useState(null);\n  const [userId, setUserId] = useState(null);\n\n  useEffect(() =\u003e {\n    // Fetch chat and user data from your API\n    const fetchChatData = async () =\u003e {\n      const response = await fetch('/api/current-chat');\n      const data = await response.json();\n      setChatId(data.chatId);\n      setUserId(data.userId);\n    };\n    \n    fetchChatData();\n  }, []);\n\n  // Only create subscription once we have the required data\n  useEffect(() =\u003e {\n    if (chatId \u0026\u0026 userId) {\n      // Create your ActionCable subscription here\n    }\n  }, [chatId, userId]);\n}\n```\n\n### 3. From Authentication Context\n```javascript\nfunction Chat({ chatId }) {\n  const { currentUser } = useAuth(); // From your auth provider\n  const userId = currentUser?.id;\n\n  // Use chatId (from props/navigation) and userId (from auth context)\n}\n```\n\n### 4. From Global State (Redux/Context)\n```javascript\nfunction Chat() {\n  const chatId = useSelector(state =\u003e state.chat.currentChatId);\n  const userId = useSelector(state =\u003e state.auth.userId);\n\n  // Use values from your global state\n}\n```\n\nThe key point is that you need to obtain these identifiers through your app's normal data flow (API calls, navigation, authentication, etc.) before creating the ActionCable subscription.\n\n## Methods\n\n`ActionCable` top level methods:\n\n- **`.createConsumer(websocketUrl, headers = {})`**  - create actionCable consumer and start connecting.\n  - `websocketUrl` - url to your Rails app's `cable` endpoint (can be a string or a function that returns a string)\n  - `headers` - headers to send with connection request (can be an object or a function that returns an object)\n- **`.getOrCreateConsumer(websocketUrl, headers = {})`**  - get existing consumer or create a new one. Prevents multiple connections to the same URL.\n  - Returns existing active consumer if available, otherwise creates a new one\n  - Automatically cleans up disconnected consumers\n  - Useful for preventing duplicate connections during app restarts or hot reloads\n- **`.disconnectConsumer(websocketUrl, headers = {})`**  - disconnect and remove a specific consumer from cache.\n  - Returns `true` if consumer was found and disconnected, `false` otherwise\n- **`.startDebugging()`**  - start logging\n- **`.stopDebugging()`**  - stop logging\n\n`ActionCable` instance methods:\n\n- **`.open()`**  - try connect\n- **`.connection.isOpen()`**  - check if `connected`\n- **`.connection.isActive()`**  - check if `connected` or `connecting`\n- **`.subscriptions.create({ channel, otherParams... })`**  - create subscription to Rails app\n- **`.disconnect()`**  - disconnects from Rails app\n\n\n`Cable` instance methods:\n\n- **`.setChannel(name, actionCable.subscriptions.create())`**  - set channel to get it later\n- **`.channel(name)`**  - get channel by name\n\n`channel` methods:\n\n- **`.perform(action, data)`**  - send message to channel. action - `string`, data - `json`\n- **`.removeListener(eventName, eventListener)`**  - unsubscribe from event\n- **`.unsubscribe()`**  - unsubscribe from channel\n- **`.on(eventName, eventListener)`**  - subscribe to events. eventName can be `received`, `connected`, `rejected`, `disconnected`, `error` or value of `data.action` attribute from channel message payload.\n\nCustom action example:\n```rb\n{\n  \"identifier\": \"{\\\"channel\\\":\\\"ChatChannel\\\",\\\"id\\\":42}\",\n  \"command\": \"message\",\n  \"data\": \"{\\\"action\\\":\\\"speak\\\",\\\"text\\\":\\\"hello!\\\"}\"\n}\n```\nAbove message will be emited with `eventName = 'speak'`\n\n## Preventing Multiple Connections\n\nWhen developing React Native apps, you may encounter duplicate ActionCable connections during:\n- Hot reloads/Fast Refresh\n- App restarts\n- Component remounting\n\nThis can cause multiple event listeners and duplicate message handling. To prevent this:\n\n### Use `getOrCreateConsumer` instead of `createConsumer`\n\n```javascript\n// ❌ This creates a new connection every time\nconst actionCable = ActionCable.createConsumer('ws://localhost:3000/cable');\n\n// ✅ This reuses existing connections\nconst actionCable = ActionCable.getOrCreateConsumer('ws://localhost:3000/cable');\n```\n\n### Implement proper cleanup\n\n```javascript\n// In your component\nclass MyComponent extends Component {\n  componentDidMount() {\n    this.setupActionCable();\n  }\n  \n  componentWillUnmount() {\n    this.cleanupActionCable();\n  }\n  \n  setupActionCable() {\n    // This will reuse existing connection if available\n    this.consumer = ActionCable.getOrCreateConsumer('ws://localhost:3000/cable');\n    // ... setup channels\n  }\n  \n  cleanupActionCable() {\n    if (this.channel) {\n      this.channel.removeListener('received', this.handleReceived);\n      this.channel.unsubscribe();\n    }\n  }\n}\n```\n\n### Check connection status\n\n```javascript\n// Check if consumer is active before creating subscriptions\nif (actionCable.connection.isActive()) {\n  console.log('Already connected');\n} else {\n  console.log('Not connected');\n}\n```\n\n### Manual consumer cleanup\n\n```javascript\n// Disconnect and remove a specific consumer from cache\nActionCable.disconnectConsumer('ws://localhost:3000/cable');\n```\n\n## Connection Error Handling\n\nThe library now supports listening to WebSocket connection errors. This is useful for handling scenarios such as:\n\n- No internet connection\n- Wrong host/URL\n- Server unavailable\n- Authentication failures\n\n```javascript\nchannel.on('error', (error) =\u003e {\n  console.log('WebSocket connection error:', error);\n  // Handle error (show offline message, retry logic, etc.)\n});\n```\n\nThe `error` event will be triggered when the underlying WebSocket connection encounters an error, allowing you to implement custom error handling logic in your application.\n\n## Testing with Jest\n\nThis library can be easily mocked for Jest tests. Here are several approaches:\n\n### Quick Setup\n\nFor most use cases, you can use this comprehensive mock:\n\n```javascript\njest.mock('@kesha-antonov/react-native-action-cable', () =\u003e {\n  const createMockSubscription = () =\u003e ({\n    on: jest.fn().mockReturnThis(),\n    removeListener: jest.fn().mockReturnThis(),\n    perform: jest.fn(),\n    unsubscribe: jest.fn(),\n  });\n\n  const createMockConsumer = () =\u003e ({\n    subscriptions: {\n      create: jest.fn().mockImplementation(() =\u003e createMockSubscription()),\n    },\n    connection: {\n      isActive: jest.fn().mockReturnValue(true),\n      isOpen: jest.fn().mockReturnValue(true),\n    },\n    connect: jest.fn(),\n    disconnect: jest.fn(),\n  });\n\n  return {\n    ActionCable: {\n      createConsumer: jest.fn().mockImplementation(() =\u003e createMockConsumer()),\n      startDebugging: jest.fn(),\n      stopDebugging: jest.fn(),\n      log: jest.fn(),\n    },\n    Cable: jest.fn().mockImplementation(() =\u003e ({\n      channels: {},\n      channel: jest.fn(),\n      setChannel: jest.fn(),\n    })),\n  };\n});\n```\n\n### Testing Examples\n\n```javascript\n// Test component that uses ActionCable\nit('should create consumer and subscription', () =\u003e {\n  render(\u003cYourComponent /\u003e);\n  \n  expect(ActionCable.createConsumer).toHaveBeenCalledWith('ws://localhost:3000/cable');\n});\n\nit('should handle received messages', () =\u003e {\n  const { getByText } = render(\u003cYourComponent /\u003e);\n  \n  // Get the mock subscription\n  const mockSubscription = ActionCable.createConsumer().subscriptions.create();\n  \n  // Simulate receiving a message\n  const receivedHandler = mockSubscription.on.mock.calls\n    .find(call =\u003e call[0] === 'received')[1];\n  receivedHandler({ type: 'new_message', message: 'Hello' });\n  \n  expect(getByText('Hello')).toBeTruthy();\n});\n```\n\n### Complete Examples\n\nFor comprehensive examples including:\n- Full Jest setup files\n- Example components and tests  \n- Common testing patterns\n- Troubleshooting guide\n\nCheck the [`examples/testing`](examples/testing) directory in this repository.\n\n## Complete Examples\n\n### Basic Chat Application\n\nFor a complete working example with both Rails backend and React Native frontend:\n\n**[Complete Chat App Example](examples/complete-chat-app)**\n\nThis example includes:\n- **Complete Rails Backend**: Ready-to-run Rails API with ActionCable channel\n- **Complete React Native Frontend**: Full app using this library for real-time chat\n- **Step-by-step Setup**: Detailed instructions for both backend and frontend\n- **Real-time Messaging**: Live demonstration of ActionCable integration\n\n### Apollo GraphQL Integration\n\nFor using ActionCable with GraphQL subscriptions and Apollo Client:\n\n**[Apollo GraphQL Example](examples/apollo-graphql)**\n\nThis example shows how to:\n- Set up Apollo Client with ActionCable for GraphQL subscriptions\n- Use the adapted ActionCableLink for seamless integration\n- Handle real-time GraphQL subscriptions in React Native\n- Configure Rails backend for GraphQL over ActionCable\n\n## Contributing\n\n1. Fork it ( https://github.com/kesha-antonov/react-native-action-cable/fork )\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\n## Credits\n\nObviously, 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](https://github.com/BinaryMuse/fluxxor) for implementation details and props binding.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkesha-antonov%2Freact-native-action-cable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkesha-antonov%2Freact-native-action-cable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkesha-antonov%2Freact-native-action-cable/lists"}