A working example with a simple Apollo Client Subscription.
Trying to get an Apollo Client setup to work with Next.JS version 9 using the with-apollo example and switching between apollo-link-ws and apollo-link-http by using the http-link split option: There is a hint for such a solution in this example: subscriptions-transport-ws. However, I'm trying now for days to figure out, how this works. Having overcome all errors meanwhile, and get the useQuery to work, but the useSubscription never leaves the loading state. There was another hint here: addressing such an infinite loading state. But I don't get it working.
A clear and concise description of what you want and what your use case is.
Want to use Apollo Subscriptions with Next.JS Version 9.
A clear and concise description of what you want to happen.
Based on the example with-apollo, tried to do that:
./ApolloClient.js:
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { setContext } from 'apollo-link-context';
import { getMainDefinition } from 'apollo-utilities';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { parseCookies } from 'nookies';
import * as ws from 'ws';
import fetch from 'isomorphic-unfetch';
export default function createApolloClient(initialState, ctx) {
const HASURA_HTTP_URI = `https://${SERVER}/v1/graphql`
const HASURA_WS_URI = `wss://${SERVER}/v1/graphql`
const COOKIE_JWT_TOKEN = process.env.COOKIE_JWT_TOKEN;
const token = process.browser ? parseCookies()[COOKIE_JWT_TOKEN] : '';
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
};
});
let wsLink = new WebSocketLink({
uri: HASURA_WS_URI,
options: {
lazy: true,
reconnect: true,
connectionParams: {
headers: {
Authorization: token ? `Bearer ${token}` : ''
}
}
},
webSocketImpl: ws
});
let httpLink = authLink.concat(
new HttpLink({
uri: HASURA_HTTP_URI,
credentials: 'include',
fetch: fetch
})
);
let myLink = process.browser
? split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === 'OperationDefinition' && operation === 'subscription';
},
wsLink,
httpLink
)
: httpLink;
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: myLink,
cache: new InMemoryCache().restore(initialState)
});
}
./pages/apollo_subscription.js:
import Layout from '../components/layouts/Default';
import { withApollo } from '../lib/apollo';
import { useSubscription /* as __useSubscription */ } from '@apollo/react-hooks';
import gql from 'graphql-tag';
// const useSubscription = (query, options) => (variables) => {
// const [values, setValues] = useState({ loading: true });
// __useSubscription(
// query,
// {
// ...options,
// variables,
// onSubscriptionData: (options) => {
// console.log(options)
// setValues(() => options.subscriptionData);
// }
// }
// );
// return values;
// };
const MEMBERSHIPS = gql`
subscription MySubscription {
my_clients {
client_id
}
}
`;
const ApolloSubscription = () => {
const { loading, error, data } = useSubscription(MEMBERSHIPS, { variables: {} });
React.useEffect(
() => { console.log('loading:', loading, 'data:', data, 'error:', error);
},[ loading, data, error ]);
if (loading) return <Layout pageTitle="Apollo">Loading ...</Layout>;
if (error)
return (
<Layout pageTitle="Apollo">
<pre>Error ... {JSON.stringify(error, null, 2)}</pre>
</Layout>
);
return (
<React.Fragment>
<Layout pageTitle="Apollo">
<h3>Subscriptions</h3>
<pre>
DATA:
{data && JSON.stringify(data, null, 2)}
</pre>
</Layout>
</React.Fragment>
);
};
export default withApollo({ ssr: false })(ApolloSubscription);
A clear and concise description of any alternative solutions or features you've considered.
Add any other context or screenshots about the feature request here.
Found a solution without apollo using npm swr instead...
Found this issue because I also had some initial trouble with using useSubscription. These changes seem to be working for me so far. I can use useSubscription just fine.
apolloClient.js from with-apollo example, with changes to use ws if client-side:
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import fetch from 'isomorphic-unfetch'
import { WebSocketLink } from "apollo-link-ws";
import { SubscriptionClient } from "subscriptions-transport-ws";
export default function createApolloClient(initialState, ctx) {
const ssrMode = Boolean(ctx)
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
let link
if (ssrMode) {
link = new HttpLink({
uri: "http://localhost:8080/v1/graphql", // Server URL (must be absolute)
credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
fetch,
})
} else {
const client = new SubscriptionClient("ws://localhost:8080/v1/graphql", {
reconnect: true
});
link = new WebSocketLink(client);
}
return new ApolloClient({
ssrMode,
link,
cache: new InMemoryCache().restore(initialState),
})
}
Is there any problem with this solution that I haven't bumped into yet? Or should something like this perhaps be included in the with-apollo example?
Thanks a lot! @johnniehard With your hints I got it working.
Added the authorization logic additionally.
Would be usefull having it in a with-apollo-subscription example
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import fetch from 'isomorphic-unfetch';
import { WebSocketLink } from 'apollo-link-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { parseCookies } from 'nookies';
const SERVER = process.env.GRAPHQL_SERVER;
const HTTP_URI = `https://${SERVER}/v1/graphql`;
const WS_URI = `wss://${SERVER}/v1/graphql`;
const COOKIE_JWT_TOKEN = process.env.COOKIE_JWT_TOKEN;
export default function createApolloClient(initialState, ctx) {
const ssrMode = (typeof window === 'undefined');
let link, token;
if (ssrMode) {
// on Server...
token = parseCookies(ctx)[COOKIE_JWT_TOKEN]
link = new HttpLink({
uri: HTTP_URI,
credentials: 'same-origin',
headers: {
authorization: token ? `Bearer ${token}` : ''
},
fetch
});
} else {
// on Client...
token = parseCookies()[COOKIE_JWT_TOKEN]
const client = new SubscriptionClient(
WS_URI, {
reconnect: true,
connectionParams: {
headers: {
authorization: token ? `Bearer ${token}` : ''
}
}
}
);
link = new WebSocketLink(client);
}
return new ApolloClient({
ssrMode,
link,
cache: new InMemoryCache().restore(initialState)
});
}
@tobkle what's COOKIE_JWT_TOKEN in your example? The name of the cookie?
yes
@tobkle I use auth0 as auth-provider and therefore the session-cookie is httpOnly, which means parseCookie() can't find the session-cookie on client side. I think it is a security issue if you use session-cookies which are accessable via document.cookie... more here: owasp-http-only
This is my auth-solution without cookies:
import { ApolloClient } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { onError } from 'apollo-link-error'
import fetch from 'isomorphic-unfetch'
import { WebSocketLink } from 'apollo-link-ws'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { split } from 'apollo-link'
import { getMainDefinition } from 'apollo-utilities'
let accessToken = null
const requestAccessToken = async () => {
if (accessToken) return
const res = await fetch(`${process.env.DOMAIN}/api/session`)
if (res.ok) {
const json = await res.json()
accessToken = json.accessToken
} else {
accessToken = 'public'
}
}
// return the headers to the context so httpLink can read them
const authLink = setContext(async (req, { headers }) => {
await requestAccessToken()
if (!accessToken || accessToken === 'public') {
return {
headers,
}
} else {
return {
headers: {
...headers,
Authorization: `Bearer ${accessToken}`,
},
}
}
})
// remove cached token on 401 from the server
const resetTokenLink = onError(({ networkError }) => {
if (networkError && networkError.name === 'ServerError' && networkError.statusCode === 401) {
accessToken = null
}
})
const httpLink = new HttpLink({
uri: process.env.API_URL,
credentials: 'include',
fetch,
})
const createWSLink = () => {
return new WebSocketLink(
new SubscriptionClient(process.env.API_WS, {
lazy: true,
reconnect: true,
connectionParams: async () => {
await requestAccessToken()
return {
headers: {
authorization: accessToken ? `Bearer ${accessToken}` : '',
},
}
},
})
)
}
export default function createApolloClient(initialState, ctx) {
accessToken = null
const ssrMode = typeof window === 'undefined'
let link
if (ssrMode) {
link = authLink.concat(resetTokenLink).concat(httpLink)
} else {
const wsLink = createWSLink()
link = split(
({ query }) => {
const definition = getMainDefinition(query)
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
},
resetTokenLink.concat(wsLink),
authLink.concat(resetTokenLink).concat(httpLink)
)
}
return new ApolloClient({
ssrMode,
link,
cache: new InMemoryCache().restore(initialState),
})
}
Hi @johnniehard and @tobkle, thank you for the solution, works like a charm. I noticed that for every mutation, this current set up uses the websocket setup instead of the HTTP setup. I'm wondering if this doesn't have any performance implication on the server or the app itself.
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";
import { WebSocketLink } from "apollo-link-ws";
import { SubscriptionClient } from "subscriptions-transport-ws";
import cookie from "js-cookie";
import { getMainDefinition } from "apollo-utilities";
import { split } from "apollo-link";
const URI = "http://localhost:4000";
const WS_URI = "ws://localhost:4000";
export default function createApolloClient(initialState, ctx) {
// console.log("in apolloCLient", ctx);
// let token;
let link, token, httpLink, wsLink;
const ssrMode = typeof window === "undefined";
token = cookie.get("token");
// console.log("in apolloCLient", token);
httpLink = new HttpLink({
uri: URI,
credentials: "same-origin",
headers: {
Authorization: token ? `Bearer ${token}` : ""
},
fetch
});
if (ssrMode) {
return new ApolloClient({
ssrMode,
link: httpLink,
cache: new InMemoryCache().restore(initialState)
});
} else {
// on Client...
const client = new SubscriptionClient(WS_URI, {
reconnect: true,
connectionParams: {
// headers: {
Authorization: token ? `Bearer ${token}` : ""
// }
}
});
wsLink = new WebSocketLink(client);
link = process.browser
? split(
//only create the split in the browser
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return (
kind === "OperationDefinition" && operation === "subscription"
);
},
wsLink,
httpLink
)
: httpLink;
return new ApolloClient({
ssrMode,
link,
cache: new InMemoryCache().restore(initialState)
});
}
}
For an example using Apollo client v3.0 and TypeScript, here is my version:
```import { ApolloClient, InMemoryCache, ApolloLink, HttpLink, split, OperationVariables } from '@apollo/client'
import { WebSocketLink } from '@apollo/link-ws'
import { onError } from '@apollo/link-error'
import { setContext } from '@apollo/link-context'
import { getMainDefinition } from 'apollo-utilities'
import { API_URL, WS_URL } from 'helpers/constants'
import { userService } from 'services/userService'
global.fetch = require('node-fetch')
let globalApolloClient: any = null
const wsLinkwithoutAuth = () =>
new WebSocketLink({
uri: WS_URL,
options: {
reconnect: true,
},
})
const wsLinkwithAuth = (token: string) =>
new WebSocketLink({
uri: WS_URL,
options: {
reconnect: true,
connectionParams: {
authToken: Bearer ${token},
},
},
})
function createIsomorphLink() {
return new HttpLink({
uri: API_URL,
})
}
function createWebSocketLink() {
return userService.token ? wsLinkwithAuth(userService.token) : wsLinkwithoutAuth()
}
const errorLink = onError(({ networkError, graphQLErrors }) => {
if (graphQLErrors) {
graphQLErrors.map((err) => {
console.warn(err.message)
})
}
if (networkError) {
console.warn(networkError)
}
})
const authLink = setContext((_, { headers }) => {
const token = userService.token
const authorization = token ? Bearer ${token} : null
return token
? {
headers: {
...headers,
authorization,
},
}
: {
headers: {
...headers,
},
}
})
const httpLink = ApolloLink.from([errorLink, authLink, createIsomorphLink()])
export function createApolloClient(initialState = {}) {
const ssrMode = typeof window === 'undefined'
const cache = new InMemoryCache().restore(initialState)
const link = ssrMode
? httpLink
: process.browser
? split(
({ query }: any) => {
const { kind, operation }: OperationVariables = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
createWebSocketLink(),
httpLink
)
: httpLink
return new ApolloClient({
ssrMode,
link,
cache,
})
}
export function initApolloClient(initialState = {}) {
if (typeof window === 'undefined') {
return createApolloClient(initialState)
}
if (!globalApolloClient) {
globalApolloClient = createApolloClient(initialState)
}
return globalApolloClient
}
```
how to make apollo server for subscription in client??
@david718
If you're using Apollo client, you don't need an Apollo server specifically - any GraphQL server will do!
There's several ways of going about this - for example, I'm using PostGraphile, which creates a GraphQL server based on a PostgreSQL DB)
For more information around Apollo Server specifically, here's a good place to start:
https://www.apollographql.com/docs/apollo-server/
@david718 I'm trying to figure out the same thing.
So far I've got in my apollo-micro-server
/pages/api/graphql.ts:
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
subscriptions: {
path: '/api/graphql',
keepAlive: 9000,
onConnect: () => console.log('connected'),
onDisconnect: () => console.log('disconnected'),
},
But it seems somehow, you've got to do something with apolloServer.installSubscriptionHandlers() to handle websocket requests.
I can't really figure that out.
@tobkle How did you do this? You said you used swr (which I am using for my other graphql requests), but that example doesn't set anything up with the server.
Most helpful comment
Thanks a lot! @johnniehard With your hints I got it working.
Added the authorization logic additionally.
Would be usefull having it in a with-apollo-subscription example