Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setContext to async import a library to generate the link #11361

Closed
arahman4710 opened this issue Nov 11, 2023 · 19 comments
Closed

setContext to async import a library to generate the link #11361

arahman4710 opened this issue Nov 11, 2023 · 19 comments
Assignees

Comments

@arahman4710
Copy link

arahman4710 commented Nov 11, 2023

Hey all,

I'm using action cable + next.js and importing it normally resulted in a ReferenceError: self is not defined so i decided to dynamically import it to try to fix it.

I saw in the docs that setContext is how async processing for links should work so I'm using setContext and then dynamically importing action cable and then returning the action cable link. However, it doesn't seem to be triggering the link properly. Does anyone know what i'm doing wrong? Here is my code for it:

const actionCableLink = setContext(async () => {
  console.log("IM IN HERE")
  const { createConsumer } = await import("@rails/actioncable");
  const cable = createConsumer(process.env.NEXT_PUBLIC_ACTION_CABLE_API_URL);
  return 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,
);

Thanks!

@jerelmiller
Copy link
Member

Hey @arahman4710 👋

The problem you're running into is that your actionCableLink needs to be a terminating link since it needs to handle sending the GraphQL request to the server.

setContext is not a terminating link, but since its the end of the chain, Apollo Client doesn't really know what to do. setContext is a shortcut for allowing you to access operation.getContext() further down the link chain.

I'm not super familiar with Action Cable since the last time I used rails was the Rails 4 days before Action Cable was introduced, but looking around at the various libraries, here are a couple things that I can point out that should help.

Even if setContext worked in this example, you'd be creating new consumers for every subscription request, which, as I understand it, would create a new websocket connection for each subscription. Per the Action Cable documentation

The client of a WebSocket connection is called the connection consumer. An individual user will create one consumer-connection pair per browser tab, window, or device they have open.

You'll want to make sure you create a single consumer, which should be done outside of ApolloLink.

I'm assuming you're using GraphQL Ruby in your example, and taking a look there at the ActionCableLink example, you can see this. In fact, that example should be almost verbatim what you need to set this up. Note here how new ActionCableLink({ cable }) is passed directly to split.

Try the following and see if it works for you:

import { ApolloClient, HttpLink, ApolloLink, InMemoryCache } from '@apollo/client';
import { createConsumer } from '@rails/actioncable';
import ActionCableLink from 'graphql-ruby-client/subscriptions/ActionCableLink';

const cable = createConsumer(process.env.NEXT_PUBLIC_ACTION_CABLE_API_URL)

const hasSubscriptionOperation = ({ query: { definitions } }) => {
  return definitions.some(
    ({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription'
  )
}

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

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

@arahman4710
Copy link
Author

Hey @jerelmiller, I tried the example solution initially but got a ReferenceError: self is not defined because of action cable. I'm trying to dynamically import action cable to avoid this problem but am not sure how to create the apollo link asynchronously.

Do you have any advice for that?

Thank you!

@jerelmiller
Copy link
Member

jerelmiller commented Nov 11, 2023

I'm using action cable + next.js

I guess I should have read this first 😅

To be honest, I'm not sure these are compatible. Action Cable works with a Rails backend. Since you're using Next.js, Next.js will not know how to handle this. I'd recommend finding some blog posts that describe how to setup websockets with Next.js and figuring out what client libraries you'll need.

The first part of my previous reply still stands though. You'll need a terminating link which will send the websocket request to your server. setContext is not a terminating link and won't work here. Not sure if graphql-ws is compatible` with Next.js or not, but if so, you can use our subscriptions link.

@jerelmiller
Copy link
Member

ReferenceError: self is not defined because of action cable

Are you sure this is because of the import, or is this because you're using Action Cable with Next.js?

@arahman4710
Copy link
Author

I think its because i'm using action cable with next js but i was trying to load it asynchronously to avoid this problem (not 100% sure if that'd work though)

@jerelmiller
Copy link
Member

not sure how to create the apollo link asynchronously.

The trick here is to return an Observable from your link and before you start sending the observer notifications, you need to resolve the promise. Check out the implementation of setContext for an idea of how we achieve this. Note how we don't call forward(operation).subscribe until the promise is resolved.

i was trying to load it asynchronously to avoid this problem (not 100% sure if that'd work though)

I'm not sure this will solve anything to be honest. Unless there is something I'm not aware of where Next.js has compatibility with Action Cable, you're going to run into the same issue, it just will happen "later".

@jerelmiller
Copy link
Member

Out of sheer curiosity, do you have a resource I can look at that recommends or shows Action Cable working with Next.js? I'm curious how you arrived at the conclusion to use these two together.

Totally fine if you'd rather not answer this question! Again, just super curious what material is out there so I can better understand 🙂

@arahman4710
Copy link
Author

I don't have a specific resource. I have a react/next js app for the FE and a rails app for the BE and wanted to introduce websockets. The rails recommended solution for web sockets is action cable which is why I started to look into that.

Does that help?

@arahman4710
Copy link
Author

I was following this blog post: https://medium.com/@jerridan/implementing-graphql-subscriptions-in-rails-and-react-9e05ca8d6b20 (it doesn't specifically mention next js though)

@jerelmiller
Copy link
Member

I have a react/next js app for the FE and a rails app for the BE

Ah ok this is useful context that I was missing. I'm used to seeing Next.js being the "full stack" solution, which includes Next.js being your server, not just the frontend part, so this makes sense! To be honest, I'm not super familiar with Next.js and all its capabilities, so I didn't realize you could just use it as a frontend.

In that case, I take back what I said. As long as your frontend app is communicating to the Rails server, Action Cable should work just fine!

Back to this error:

ReferenceError: self is not defined because of action cable

Does this happen on the import, or when you call createConsumer?

@jerelmiller
Copy link
Member

jerelmiller commented Nov 11, 2023

I just found this post about that error: https://stackoverflow.com/questions/66096260/why-am-i-getting-referenceerror-self-is-not-defined-when-i-import-a-client-side

Does this match what you're seeing?

If so, now I finally understand why you're trying to do the dynamic import. Thanks for taking this ride with me 😆. I'll followup with an idea of how you might get this to work.

@jerelmiller
Copy link
Member

jerelmiller commented Nov 11, 2023

Alright, see if something like this works for you (warning, untested).

import { setContext } from '@apollo/client/link/context';

// We only want to create the consumer once, so cache it after its created
let consumer;
async function getActionCableConsumer() {
  if (consumer) {
    return consumer;
  }

  const { createConsumer } = await import('@rails/actioncable');
  consumer = createConsumer(process.env.NEXT_PUBLIC_ACTION_CABLE_API_URL)

  return consumer;
}

// We can use `setContext` to create the cable async and pass it to the terminating link
const setCableLink = setContext(async () => ({
  cable: await getActionCableConsumer()
}))

// Similarly, we only want to create the action cable link once
let actionCableLink;
const getActionCableLink = ({ cable }) => {
  actionCableLink ||= new ActionCableLink({ cable });

  return actionCableLink;
}

// Since we can't create the `ActionCableLink` until we have the cable, we wrap it
// with a custom `ApolloLink` and execute it here. This is our terminating link
const actionCableLink = new ApolloLink((operation) => {
  // Pull the cable off of context which you set in the `setCableLink`
  const { cable } = operation.getContext();

  return ApolloLink.execute(getActionCableLink({ cable }), operation)
});

// Ensure your subscription link is a combination of `setCableLink` and `actionCableLink`
const subscriptionLink = setCableLink.concat(actionCableLink);

const link = ApolloLink.split(
  hasSubscriptionOperation,
  subscriptionLink,
  httpLink
);

Try this out and let me know if this works for you!

@arahman4710
Copy link
Author

Gonna try it out in a bit, thanks for the help!

@arahman4710
Copy link
Author

Hey @jerelmiller, I tried it out but I don't think its working as intended.

I would expect a request to process.env.NEXT_PUBLIC_ACTION_CABLE_API_URL with the subscription graphql data but i'm not seeing that.

Any other ideas for how to debug or try out?

Thanks!

@arahman4710
Copy link
Author

I have verified that all the functions you've written are being called but it doesn't seem like the end result is happening the way I'd expect it to

@arahman4710
Copy link
Author

arahman4710 commented Nov 12, 2023

Stepped through the code a bit more. It seems like ApolloLink.execute(getActionCableLink({ cable })) is causing the following error:

ApolloLink.js:45 Uncaught TypeError: Cannot read properties of undefined (reading 'context')
    at ApolloLink.execute (webpack-internal:///(app-pages-browser)/./node_modules/@apollo/client/link/core/ApolloLink.js:54:105)
    at eval (eval at <anonymous> (webpack-internal:///(app-pages-browser)/./src/lib/apolloLinks.ts), <anonymous>:1:5)
    at ApolloLink.eval [as request] (webpack-internal:///(app-pages-browser)/./src/lib/apolloLinks.ts:95:5)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/@apollo/client/link/core/ApolloLink.js:71:37)
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/@apollo/client/link/context/index.js:23:26)

I'm reading the docs on a terminating link and it seems like we need to "send your composed GraphQL operation to the destination that executes it (usually a GraphQL server) and returning an ExecutionResult"

@arahman4710
Copy link
Author

Actually, think i got it working with return ApolloLink.execute(getActionCableLink({ cable }), operation)

Thanks for the help!

@jerelmiller jerelmiller self-assigned this Nov 14, 2023
@jerelmiller
Copy link
Member

@arahman4710 ah thank you! I totally forgot that operation argument in my example. I've updated it above. Glad its working for you! I'll go ahead and close this issue out.

Don't hesitate to reach out if you need any more help! You may also find our discord server and/or community forums helpful as well.

Copy link
Contributor

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
For general questions, we recommend using StackOverflow or our discord server.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants