I'm building a simple todo app using React, Apollo and react-apollo-hooks for hooks support, but the useSubscription hook doesnt fire.
I know the actual backend stuff works, because I have a graphiql app set up, and whenever I save a todo, the todoCreated event shows up in graphiql. I also know that the websocket-setup is working properly, because the queries & mutations are going through the websocket. I'm using Elixir, Phoenix, Absinthe, by the way, for the backend stuff.
Here's the Todo-app component:
import React, { useState } from 'react';
import gql from 'graphql-tag';
import { useQuery, useMutation, useSubscription } from 'react-apollo-hooks';
import styles from 'styles.css';
const TODO_FRAGMENT = gql`
fragment TodoFields on Todo {
id
description
}
`;
const GET_TODOS = gql`
{
todos {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const SAVE_TODO = gql`
mutation createTodo($description: String!) {
createTodo(description: $description) {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const DELETE_TODO = gql`
mutation deleteTodo($id: ID!) {
deleteTodo(id: $id) {
id
}
}
`;
const NEW_TODO_SUBSCRIPTION = gql`
subscription {
todoCreated {
...TodoFields
}
}
${TODO_FRAGMENT}
`;
const Todos = () => {
const [inputValue, setInputValue] = useState('');
const { data, error, loading } = useQuery(GET_TODOS);
const saveTodo = useMutation(SAVE_TODO, {
update: (proxy, mutationResult) => {
proxy.writeQuery({
query: GET_TODOS,
data: { todos: data.todos.concat([mutationResult.data.createTodo]) },
});
},
});
const deleteTodo = useMutation(DELETE_TODO, {
update: (proxy, mutationResult) => {
const id = mutationResult.data.deleteTodo.id
proxy.writeQuery({
query: GET_TODOS,
data: { todos: data.todos.filter(item => item.id !== id) },
});
},
});
const subData = useSubscription(NEW_TODO_SUBSCRIPTION);
console.log(subData);
if (loading) {
return <div>Loading...</div>;
};
if (error) {
return <div>Error! {error.message}</div>;
};
return (
<>
<h1>Todos</h1>
{data.todos.map((item) => (
<div key={item.id} className={styles.item}>
<button onClick={() => {
deleteTodo({
variables: {
id: item.id,
},
});
}}>Delete</button>
{' '}
{item.description}
</div>
))}
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
type="text"
/>
<button onClick={() => {
saveTodo({
variables: {
description: inputValue,
},
});
setInputValue('');
}}>Save</button>
</>
);
};
export default Todos;
And here's the root component:
import React from 'react';
import { ApolloProvider } from 'react-apollo';
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
import Todos from 'components/Todos';
import apolloClient from 'config/apolloClient';
const App = () => (
<ApolloHooksProvider client={apolloClient}>
<Todos />
</ApolloHooksProvider>
);
export default App;
Anyone have a clue on what I seem to be doing wrong?
Sorry, I figured it out, it was a silly mistake on my part. The problem seems to have been with my apolloClient setup:
import { split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { onError } from 'apollo-link-error';
import { ApolloLink } from 'apollo-link';
import absintheSocketLink from 'config/absintheSocketLink';
const apolloClient = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
),
);
if (networkError) console.log(`[Network error]: ${networkError}`);
}),
split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
new HttpLink({
uri: 'http://localhost:4000/api/graphql',
credentials: 'same-origin'
}),
absintheSocketLink,
),
]),
cache: new InMemoryCache()
});
export default apolloClient;
The error in the code above is the fact that the line
absintheSocketLink,
is in the wrong place. It should've been before the HttpLink.
Silly me.
I had the same issue my subscription was always sending null data and i had a silly mistake as well.
Related
I am working with apollo graphql client. The subscription in the server is working fine watching for changes.
But in the client, I am not able to log data.
I also tried to mutate but still its resulting in the same thing.
useSubscription(BOOK_ADDED, {
onData: ({ data }) => {
console.log(data)
}
})
The above code doesn't log anything out.
But,
const value = useSubscription(BOOK_ADDED, {
onData: ({ data }) => {
console.log(data)
}
})
console.log(value)
The above code seems to work fine logging out a value.
I am attaching a few codes below for more clarity.
index.js or apollo setup:
import ReactDOM from 'react-dom'
import App from './App'
import {
ApolloClient,
ApolloProvider,
HttpLink,
InMemoryCache,
split,
} from '#apollo/client'
import { setContext } from '#apollo/client/link/context'
import { getMainDefinition } from '#apollo/client/utilities'
import { GraphQLWsLink } from '#apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
import Assess from './Asses'
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('library-user-token')
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
const httpLink = new HttpLink({ uri: 'http://localhost:4002' })
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4002',
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
authLink.concat(httpLink)
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
ReactDOM.render(
<ApolloProvider client={client}>
<Assess />
</ApolloProvider>,
document.getElementById('root')
)
App.js
//App.js
import { useSubscription, useQuery } from "#apollo/client";
import { ALL_BOOKS, BOOK_ADDED } from "./queries";
const App = () => {
console.log(BOOK_ADDED);
const result = useQuery(ALL_BOOKS);
useSubscription(BOOK_ADDED, {
onData: ({ data }) => {
console.log(data);
},
});
console.log(result)
if(result.loading){
return null
}
return (
<div>
{result?.data?.allBooks.map((r) => (
<li key={r.id}>{r.title}</li>
))}
</div>
);
};
export default App
The query and fragment:
const BOOK_DETAILS = gql`
fragment BookDetails on Books {
title
author {
name
}
published
genres
id
}
`;
export const BOOK_ADDED = gql`
subscription {
bookAdded {
...BookDetails
}
}
${BOOK_DETAILS}
`;
After reading the changelogs of #apollo/client. I got to know that the method demonstrated in question is only useful when the version of #apollo/client is >=3.7.0. As my version was #3.6.7 it wasn't logging the value out.
Earlier than this version the function required an onSubscriptionData callback
to perform the same operation, which is now deprecated. I have still demonstrated it below as someone using version <#3.7.0 might find it useful.
useSubscription(BOOK_ADDED,{
onSubscriptionData: ({subscriptionData: data}) =>{
console.log(data)
}
})
You may read the change log here.
I'm new to graphql I'm building real time chat app. Currently I'm making it offline first.
Using react as front-end.
I'm currently caching the data on localStorage using apollo3-cache-persist. But How do I query the cache data instead of server (when I'm offline) also I want to add messages to the localStorage while I'm offline.
Display the optimistic response when the device is online I want to send the pending data to the server.
my ApolloProvider.js file in client folder
import React from "react";
import {
ApolloClient,
InMemoryCache,
ApolloProvider as Provider,
createHttpLink,
ApolloLink,
split,
} from "#apollo/client";
import { setContext } from "#apollo/client/link/context";
import { RetryLink } from "#apollo/client/link/retry";
import { persistCache, LocalStorageWrapper } from "apollo3-cache-persist";
import { WebSocketLink } from "#apollo/client/link/ws";
import { getMainDefinition } from "#apollo/client/utilities";
import QueueLink from "apollo-link-queue";
let httpLink = createHttpLink({
uri: "http://localhost:4000/",
});
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("token");
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
});
httpLink = authLink.concat(httpLink);
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: {
reconnect: true,
connectionParams: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
},
});
const link = new RetryLink();
const queueLink = new QueueLink();
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const cache = new InMemoryCache();
const fun = async () =>
await persistCache({
cache,
storage: new LocalStorageWrapper(window.localStorage),
});
fun();
const client = new ApolloClient({
// link: splitLink,
link: ApolloLink.from([splitLink, queueLink, link]),
cache,
name: "chat-app",
version: "1.0.0",
queryDeduplication: false,
defaultOptions: {
watchQuery: {
fetchPolicy: "cache-and-network",
},
},
});
export default function ApolloProvider(props) {
return <Provider client={client} {...props} />;
}
my messages.js file
import React, { Fragment, useEffect, useState } from "react";
import { gql, useLazyQuery, useMutation, InMemoryCache } from "#apollo/client";
import { Col, Form } from "react-bootstrap";
import { useMessageDispatch, useMessageState } from "../../context/message";
import uuid from "react-uuid";
import Message from "./Message";
const SEND_MESSAGE = gql`
mutation sendMessage($uuid: String, $to: String!, $content: String!) {
sendMessage(uuid: $uuid, to: $to, content: $content) {
uuid
from
to
content
createdAt
hasSeen
hasSent
}
}
`;
const GET_MESSAGES = gql`
query getMessages($from: String!) {
getMessages(from: $from) {
uuid
from
to
content
createdAt
hasSeen
}
}
`;
export default function Messages() {
const { users } = useMessageState();
const dispatch = useMessageDispatch();
const [content, setContent] = useState("");
const selectedUser = users?.find((u) => u.selected === true);
const messages = selectedUser?.messages;
const [getMessages, { loading: messagesLoading, data: messagesData }] =
useLazyQuery(GET_MESSAGES, {
update(cache) {
cache.readFragment({});
console.log("reading");
},
});
const [sendMessage] = useMutation(SEND_MESSAGE, {
update(cache, { data: { sendMessage } }) {
cache.modify({
fields: {
getMessages(existingMsg) {
console.log(existingMsg);
const newMsgRef = cache.writeFragment({
data: sendMessage,
fragment: gql`
fragment sendNewMessage on Mutation {
uuid
to
from
content
hasSeen
hasSent
}
`,
});
return existingMsg.push(newMsgRef);
},
},
});
},
onError: (err) => console.log(err),
});
useEffect(() => {
if (selectedUser && !selectedUser.messages) {
getMessages({ variables: { from: selectedUser.username } });
}
}, [selectedUser]);
useEffect(() => {
if (messagesData) {
dispatch({
type: "SET_USER_MESSAGES",
payload: {
username: selectedUser.username,
messages: messagesData.getMessages,
},
});
}
}, [messagesData]);
const submitMessage = (e) => {
e.preventDefault();
if (content.trim() === "" || !selectedUser) return;
let id = uuid();
sendMessage({
variables: { uuid: id, to: selectedUser.username, content },
optimisticResponse: {
sendMessage: {
__typename: "Mutation",
uuid: id,
from: "User",
to: selectedUser.username,
content,
hasSent: false,
hasSeen: false,
createdAt: Date.now(),
},
},
});
setContent("");
};
// Displaying helper text and styling
let selectedChatMarkup;
if (!messages && !messagesLoading) {
selectedChatMarkup = <p className="info-text"> Select a friend</p>;
} else if (messagesLoading) {
selectedChatMarkup = <p className="info-text"> Loading..</p>;
} else if (messages.length > 0) {
selectedChatMarkup = messages.map((message, index) => (
<Fragment key={message.uuid}>
<Message message={message} />
{index === messages.length - 1 && (
<div className="invisible">
<hr className="m-0" />
</div>
)}
</Fragment>
));
} else if (messages.length === 0) {
selectedChatMarkup = (
<p className="info-text">
You are now connected! send your first message!
</p>
);
}
return (
<Col xs={10} md={8}>
<div className="messages-box d-flex flex-column-reverse">
{selectedChatMarkup}
</div>
<div>
<Form onSubmit={submitMessage}>
<Form.Group className="d-flex align-items-center">
<Form.Control
type="text"
className="message-input rounded-pill p-4 bg-secondary border-0"
placeholder="Type a message.."
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<i
className="fas fa-regular fa-paper-plane fa-2x text-primary ml-2"
onClick={submitMessage}
role="button"
></i>
</Form.Group>
</Form>
</div>
</Col>
);
}
But I'm currently getting this error when I try to send the message
react_devtools_backend.js:3973 Invariant Violation: Could not identify object {"uuid":"4855ffc-6b7b-d7c8-a68-2ae0162f80a","from":"User","to":"Fire","content":"example text","createdAt":1648881891383,"hasSeen":false,"hasSent":false,"__typename":"Mutation"}
Also getting error from the mutation error log
Error: Could not identify object {"__typename":"Message","uuid":"4855ffc-6b7b-d7c8-a68-2ae0162f80a","from":"Alan","to":"Fire","content":"example text","createdAt":"2022-04-02T06:44:51.807Z","hasSeen":false,"hasSent":false}
Apollo uses by default __typename and id fields to normalise cache. So in your case, Apollo won't recognise your uuid property as a cache identifier. You can change the uuid uuid property, or add keyFields to your InMemoryCache config.
I am new to NextJS. I have a page that needs to display real-time data pulled from a Hasura GraphQL backend.
In other non-NextJS apps, I have used GraphQL subscriptions with the Apollo client library. Under the hood, this uses websockets.
I can get GraphQL working in NextJS when it's not using subscriptions. I'm pretty sure this is running on the server-side:
import React from "react";
import { AppProps } from "next/app";
import withApollo from 'next-with-apollo';
import { ApolloProvider } from '#apollo/react-hooks';
import ApolloClient, { InMemoryCache } from 'apollo-boost';
import { getToken } from "../util/auth";
interface Props extends AppProps {
apollo: any
}
const App: React.FC<Props> = ({ Component, pageProps, apollo }) => (
<ApolloProvider client={apollo}>
<Component {...pageProps}/>
</ApolloProvider>
);
export default withApollo(({ initialState }) => new ApolloClient({
uri: "https://my_hasura_instance.com/v1/graphql",
cache: new InMemoryCache().restore(initialState || {}),
request: (operation: any) => {
const token = getToken();
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
});
}
}))(App);
And I use it this way:
import { useQuery } from '#apollo/react-hooks';
import { gql } from 'apollo-boost';
const myQuery = gql`
query {
...
}
`;
const MyComponent: React.FC = () => {
const { data } = useQuery(myQuery);
return <p>{JSON.stringify(data)}</p>
}
However, I would instead like to do this:
import { useSubscription } from '#apollo/react-hooks';
import { gql } from 'apollo-boost';
const myQuery = gql`
subscription {
...
}
`;
const MyComponent: React.FC = () => {
const { data } = useSubscription(myQuery);
return <p>{JSON.stringify(data)}</p>
}
What I've tried
I've tried splitting the HttpLink and WebsocketLink elements in the ApolloClient, like so:
import React from "react";
import { AppProps } from "next/app";
import { ApolloProvider } from '#apollo/react-hooks';
import withApollo from 'next-with-apollo';
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { getToken } from "../util/auth";
interface Props extends AppProps {
apollo: any
}
const App: React.FC<Props> = ({ Component, pageProps, apollo }) => (
<ApolloProvider client={apollo}>
<Component {...pageProps}/>
</ApolloProvider>
);
const wsLink = new WebSocketLink({
uri: "wss://my_hasura_instance.com/v1/graphql",
options: {
reconnect: true,
timeout: 10000,
connectionParams: () => ({
headers: {
authorization: getToken() ? `Bearer ${getToken()}` : ""
}
})
},
});
const httpLink = new HttpLink({
uri: "https://hasura-g3uc.onrender.com/v1/graphql",
});
const link = process.browser ? split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
) : httpLink;
export default withApollo(({ initialState }) => new ApolloClient({
link: link,
cache: new InMemoryCache().restore(initialState || {}),
}))(App);
But when I load the page, I get an Internal Server Error, and this error in the terminal:
Error: Unable to find native implementation, or alternative implementation for WebSocket!
It seems to me that the ApolloClient is then being generated on the server-side, where there is no WebSocket implementation. How can I make this happen on the client-side?
Found workaround to make it work, take look at this answer https://github.com/apollographql/subscriptions-transport-ws/issues/333#issuecomment-359261024
the reason was due to server-side rendering; these statements must run in the browser, so we test if we have process.browser !!
relevant section from the attached github link:
const wsLink = process.browser ? new WebSocketLink({ // if you instantiate in the server, the error will be thrown
uri: `ws://localhost:4000/subscriptions`,
options: {
reconnect: true
}
}) : null;
const httplink = new HttpLink({
uri: 'http://localhost:3000/graphql',
credentials: 'same-origin'
});
const 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;
This answer seems to be more actual
https://github.com/apollographql/subscriptions-transport-ws/issues/333#issuecomment-775578327
You should install ws by npm i ws and add webSocketImpl: ws to WebSocketLink argument.
import ws from 'ws';
const wsLink = new WebSocketLink({
uri: endpoints.ws,
options: {
reconnect: true,
connectionParams: () => ({
...getToken() && {Authorization: getToken()}
})
},
webSocketImpl: ws
});
Solution: Make wsLink a function variable like the code below.
// src/apollo.ts
import { ApolloClient, HttpLink, InMemoryCache } from "#apollo/client";
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
});
const wsLink = () => {
return new GraphQLWsLink(createClient({
url: 'ws://localhost:3000/graphql'
}));
}
export const apolloClient = new ApolloClient({
link: typeof window === 'undefined' ? httpLink : wsLink(),
cache: new InMemoryCache(),
});
// pages/_app.tsx
import { ApolloProvider } from "#apollo/client";
import { apolloClient } from "../src/apollo";
function MyApp({ Component, pageProps }) {
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}
I'm using nextJS V9.5.5 with wp-graphql and apolloClient to get data from WordPress. Everything works fine, but when I try to return context (in the purpose of getting query) from getStaticProps() like it's described in docs, it returns an empty object.
Custom App:
import React from "react";
import getConfig from "next/config";
import LayoutOuter from "../components/LayoutOuter";
import "bootstrap/dist/css/bootstrap.css";
import { ApolloProvider } from "#apollo/client";
import { useApollo } from "../lib/apolloClient";
import { initializeApollo } from "../lib/apolloClient";
import { gql } from "#apollo/client";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const { DOMAIN } = publicRuntimeConfig;
function CustomApp({ pageProps, Component, props }) {
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ApolloProvider client={apolloClient}>
{console.log("_app", props)}
<LayoutOuter>
<Component {...pageProps} />
</LayoutOuter>
</ApolloProvider>
);
}
CustomApp.getInitialProps = async (ctx) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
{
// my graphql query here
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
ctx: JSON.stringify(ctx),
},
};
};
export default CustomApp;
One of the page:
import React, { Component, useEffect, useState } from "react";
import getConfig from "next/config";
import { NextSeo } from "next-seo";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const { DOMAIN, SITENAME } = publicRuntimeConfig;
import { initializeApollo } from "../lib/apolloClient";
import { gql } from "#apollo/client";
import "./services.module.scss";
const Home = (props) => {
let currentPage = Object.values(props.initialApolloState.ROOT_QUERY)[1];
const {
title,
metadesc,
metaRobotsNoindex,
metaRobotsNofollow,
metaRobotsAdv,
opengraphTitle,
opengraphDescription,
opengraphImage,
twitterTitle,
twitterDescription,
twitterImage,
} = currentPage.seo;
return (
<>
{console.log("project", props)}
<NextSeo
noindex={metaRobotsNoindex}
nofollow={metaRobotsNofollow}
title={title != "" ? title : `${props.data.pagetitle} - ${SITENAME}`}
description={metadesc}
canonical={DOMAIN}
openGraph={{
url: DOMAIN,
title:
opengraphTitle != ""
? opengraphTitle
: `${props.data.pagetitle} - Garrison Collection`,
description: opengraphDescription,
images: [
{
url: opengraphImage,
width: 800,
height: 600,
alt: { SITENAME },
},
],
site_name: { SITENAME },
}}
/>
<p>works</p>
</>
);
};
export async function getStaticProps(context) {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
{
project(id: "ca-souls", idType: SLUG) {
seo {
canonical
metaDesc
metaKeywords
metaRobotsNofollow
metaRobotsNoindex
opengraphAuthor
opengraphDescription
opengraphModifiedTime
opengraphPublishedTime
opengraphPublisher
opengraphSiteName
opengraphTitle
opengraphType
opengraphUrl
title
twitterDescription
twitterTitle
}
}
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
context: JSON.stringify(context) || null,
},
revalidate: 1,
};
}
export default Home;
Here is the log result:
How could I get context.query?
The context parameter includes previewData contains the preview data set by setPreviewData. This means including function, therefore, unable to serealize. Take values out from context.params.
I am new to NextJS. I have a page that needs to display real-time data pulled from a Hasura GraphQL backend.
In other non-NextJS apps, I have used GraphQL subscriptions with the Apollo client library. Under the hood, this uses websockets.
I can get GraphQL working in NextJS when it's not using subscriptions. I'm pretty sure this is running on the server-side:
import React from "react";
import { AppProps } from "next/app";
import withApollo from 'next-with-apollo';
import { ApolloProvider } from '#apollo/react-hooks';
import ApolloClient, { InMemoryCache } from 'apollo-boost';
import { getToken } from "../util/auth";
interface Props extends AppProps {
apollo: any
}
const App: React.FC<Props> = ({ Component, pageProps, apollo }) => (
<ApolloProvider client={apollo}>
<Component {...pageProps}/>
</ApolloProvider>
);
export default withApollo(({ initialState }) => new ApolloClient({
uri: "https://my_hasura_instance.com/v1/graphql",
cache: new InMemoryCache().restore(initialState || {}),
request: (operation: any) => {
const token = getToken();
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
});
}
}))(App);
And I use it this way:
import { useQuery } from '#apollo/react-hooks';
import { gql } from 'apollo-boost';
const myQuery = gql`
query {
...
}
`;
const MyComponent: React.FC = () => {
const { data } = useQuery(myQuery);
return <p>{JSON.stringify(data)}</p>
}
However, I would instead like to do this:
import { useSubscription } from '#apollo/react-hooks';
import { gql } from 'apollo-boost';
const myQuery = gql`
subscription {
...
}
`;
const MyComponent: React.FC = () => {
const { data } = useSubscription(myQuery);
return <p>{JSON.stringify(data)}</p>
}
What I've tried
I've tried splitting the HttpLink and WebsocketLink elements in the ApolloClient, like so:
import React from "react";
import { AppProps } from "next/app";
import { ApolloProvider } from '#apollo/react-hooks';
import withApollo from 'next-with-apollo';
import { InMemoryCache } from "apollo-cache-inmemory";
import ApolloClient from "apollo-client";
import { split } from 'apollo-link';
import { HttpLink } from 'apollo-link-http';
import { WebSocketLink } from 'apollo-link-ws';
import { getMainDefinition } from 'apollo-utilities';
import { getToken } from "../util/auth";
interface Props extends AppProps {
apollo: any
}
const App: React.FC<Props> = ({ Component, pageProps, apollo }) => (
<ApolloProvider client={apollo}>
<Component {...pageProps}/>
</ApolloProvider>
);
const wsLink = new WebSocketLink({
uri: "wss://my_hasura_instance.com/v1/graphql",
options: {
reconnect: true,
timeout: 10000,
connectionParams: () => ({
headers: {
authorization: getToken() ? `Bearer ${getToken()}` : ""
}
})
},
});
const httpLink = new HttpLink({
uri: "https://hasura-g3uc.onrender.com/v1/graphql",
});
const link = process.browser ? split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
) : httpLink;
export default withApollo(({ initialState }) => new ApolloClient({
link: link,
cache: new InMemoryCache().restore(initialState || {}),
}))(App);
But when I load the page, I get an Internal Server Error, and this error in the terminal:
Error: Unable to find native implementation, or alternative implementation for WebSocket!
It seems to me that the ApolloClient is then being generated on the server-side, where there is no WebSocket implementation. How can I make this happen on the client-side?
Found workaround to make it work, take look at this answer https://github.com/apollographql/subscriptions-transport-ws/issues/333#issuecomment-359261024
the reason was due to server-side rendering; these statements must run in the browser, so we test if we have process.browser !!
relevant section from the attached github link:
const wsLink = process.browser ? new WebSocketLink({ // if you instantiate in the server, the error will be thrown
uri: `ws://localhost:4000/subscriptions`,
options: {
reconnect: true
}
}) : null;
const httplink = new HttpLink({
uri: 'http://localhost:3000/graphql',
credentials: 'same-origin'
});
const 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;
This answer seems to be more actual
https://github.com/apollographql/subscriptions-transport-ws/issues/333#issuecomment-775578327
You should install ws by npm i ws and add webSocketImpl: ws to WebSocketLink argument.
import ws from 'ws';
const wsLink = new WebSocketLink({
uri: endpoints.ws,
options: {
reconnect: true,
connectionParams: () => ({
...getToken() && {Authorization: getToken()}
})
},
webSocketImpl: ws
});
Solution: Make wsLink a function variable like the code below.
// src/apollo.ts
import { ApolloClient, HttpLink, InMemoryCache } from "#apollo/client";
import { GraphQLWsLink } from "#apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
const httpLink = new HttpLink({
uri: 'http://localhost:3000/graphql'
});
const wsLink = () => {
return new GraphQLWsLink(createClient({
url: 'ws://localhost:3000/graphql'
}));
}
export const apolloClient = new ApolloClient({
link: typeof window === 'undefined' ? httpLink : wsLink(),
cache: new InMemoryCache(),
});
// pages/_app.tsx
import { ApolloProvider } from "#apollo/client";
import { apolloClient } from "../src/apollo";
function MyApp({ Component, pageProps }) {
return (
<ApolloProvider client={apolloClient}>
<Component {...pageProps} />
</ApolloProvider>
);
}