Skip to content

Instantly share code, notes, and snippets.

@jerridan
Last active April 20, 2024 04:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jerridan/d228758527563687c65065afc2f09745 to your computer and use it in GitHub Desktop.
Save jerridan/d228758527563687c65065afc2f09745 to your computer and use it in GitHub Desktop.
Implementing GraphQL Subscriptions in Rails and React
// ...
import ActionCable from "@rails/actioncable";
import ActionCableLink from "graphql-ruby-client/subscriptions/ActionCableLink";
import { split } from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
// Setup a link for action cable
const cable = ActionCable.createConsumer(<YOUR_ACTION_CABLE_ENDPOINT>);
const actionCableLink = new ActionCableLink({ cable });
// Redirect subscriptions to the action cable link, while using the HTTP link for other queries
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === "OperationDefinition" && definition.operation === "subscription";
},
actionCableLink,
httpLink
);
// Use the new splitLink for the Apollo client
const client = new ApolloClient({
link: splitLink,
...other options
});
// ...
// ...
const client = new ApolloClient({
uri: <YOUR_GRAPHQL_ENDPOINT>,
...other options
});
// OR
const link = createHttpLink({ uri: <YOUR_GRAPHQL_ENDPOINT> });
const client = new ApolloClient({
link,
...other options
});
// ...
module Subscriptions
class BaseSubscription < GraphQL::Schema::Subscription
def current_application_context
context[:current_application_context]
end
end
end
development:
adapter: postgresql
test:
adapter: test
production:
adapter: postgresql
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
subscription OnCommentPosted {
commentPosted {
comment {
id
message
}
}
}
# A theoretical Comment class that uses a callback to trigger a GraphQL subscription update
class Comment < ApplicationRecord
after_create :trigger_comment_posted_event
private
def trigger_comment_posted_event
ApplicationSubscription.trigger("commentPosted", {}, { comment: self })
end
end
module Subscriptions
class CommentPosted < BaseSubscription
field :comment, Types::CommentType, null: false
end
end
module ApplicationCable
class Connection < ActionCable::Connection::Base
def current_application_context
@current_application_context ||= ApplicationContext.new(cookies)
end
end
end
class GraphqlChannel < ApplicationCable::Channel
def subscribed
# Store all GraphQL subscriptions the consumer is listening for on this channel
@subscription_ids = []
end
def execute(data)
query = data["query"]
variables = ensure_hash(data["variables"])
operation_name = data["operationName"]
context = {
channel: self,
current_application_context: connection.current_application_context
}
result = Schema.execute({
query: query,
context: context,
variables: variables,
operation_name: operation_name,
})
payload = {
result: result.to_h,
more: result.subscription?,
}
# Append the subscription id
@subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
transmit(payload)
end
def unsubscribed
# Delete all of the consumer's subscriptions from the GraphQL Schema
@subscription_ids.each do |sid|
Schema.subscriptions.delete_subscription(sid)
end
end
private
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
end
Rails.application.routes.draw do
# ... other rails routes
mount ActionCable.server, at: "/cable"
end
module Types
class SubscriptionType < Types::BaseObject
description "The subscription root for the GraphQL schema"
end
end
import { useSubscription } from "@apollo/client";
import filter from "lodash/filter";
import { GetComments, OnCommentPosted, OnCommentUpdated } from "queries/comment.gql"
export default useSubscriptions() {
// Subscribe to new comments
useSubscription(OnCommentPosted, {
onSubscriptionData: ({ subscriptionData: { data } }) => {
// This will be called on initial load, so we need to handle an undefined value of data
if (!data || !data.commentPosted) return;
// Read the existing comments from cache and merge in the new value
const newComment = data.commentPosted.comment;
const { comments } = client.readQuery({ query: GetComments });
const updatedComments = [newComment].concat(filter(comments, (comment) => comment.id !== newComment.id));
// Write the updated comments to cache
client.writeQuery({
query: GetComments,
data: { comments: updatedComments },
});
}
});
}
@mengqing
Copy link

Hi, just wondering what is actually in the ApplicationContext file?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment