I am using Graphql useSubscription hook to hit a websocket api but there seems to be an issue. I am only receiving data when i enter the component for the first time or when i go back to some other component and come back again, when trying to refresh majority of the times I do not get the data.
Below is my setup for the following.
/* eslint-disable flowtype/no-types-missing-file-annotation */
import Cookies from 'js-cookie'
import { split, HttpLink, InMemoryCache, ApolloClient } from '#apollo/client'
import { setContext } from '#apollo/client/link/context'
import { WebSocketLink } from '#apollo/client/link/ws'
import { getMainDefinition } from '#apollo/client/utilities'
import { onError } from 'apollo-link-error'
import { ApolloLink } from 'apollo-link'
const env = process.env.NODE_ENV
const domain = env === 'development' ? 'localhost' : '.xyz'
const url = env === 'development' ? 'https://staging-xxx.xxxx.xx' : process.env.REACT_APP_API_URL;
const wsURL = env === 'development' ? 'wss://staging-xxx.xxxxx.xx/subscriptions' : process.env.REACT_APP_WSS_URL;
const httpLink = new HttpLink({
uri: url,
credentials: 'include'
})
const authLink = setContext((_: any, { headers }: any) => {
const app_token = Cookies.get('xxxxx', { domain: domain })
let token = app_token || 'insta-checkout'
return {
headers: {
...headers,
MUDEY_AUTH_TOKEN: token
}
}
})
const wsLink = new WebSocketLink({
uri: wsURL,
options: {
reconnect: true,
connectionParams: async () => {
const app_token = await Cookies.get('xxxxx', { domain: domain })
return {
credentials: 'include',
MUDEY_AUTH_TOKEN: app_token,
Authorization: 'Basic xxxxxxxxxxxxxxxxxxx'
}
}
}
})
const link = split(
({ query }: any) => {
const definition = getMainDefinition(query)
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
},
wsLink,
authLink.concat(httpLink)
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([link])
})
export default client
Now when i go my component for the first time, i am calling the subscriptions api by
import React from "react";
import { useMutation, useSubscription } from "#apollo/react-hooks";
export const NewComponent = () => {
const {
loading: loadingPackages,
data,
error,
} = useSubscription(SUBSCRIBE_CAR_PACKAGES, {
onSubscriptionData: useCallback((res: any) => {
const {
subscribeCarJourneyPackages: { message: stopWS, data: packagesResult },
} = res.subscriptionData.data;
if (packagesResult !== null) {
console.log("packarray", packagesResult);
setIsSubsLoading(true);
}
if (stopWS === "SUBSCRIPTION_COMPLETE") {
dispatch({ type: SET_ALL_PACKAGES, payload: packArray });
setIsSubsLoading(false);
} else {
// setIsSubsLoading(true)
}
}, []),
onError: useCallback((err: any) => {
apiErrorHandler(err);
}, []),
variables: { id: journeyID },
});
return null;
};
So the response i see is
But once i start refreshing the page , i only see
So what the issue in my frontend, for not getting the response 100% of the time ? should we need to close the connection everytime we receive response ?
Also i see the subscription api hitting even when i am in my homepage, where ideally it should hit in the results page where i want it, do this happens the moment we define connection and is it normal?
I know it's late but it maybe help someone else !
try giving fetchPolicy: 'cache-and-network' as option below your variables field.
like this :
variables: {... your variables },
fetchPolicy: 'cache-and-network'
Related
I am try to use #apollo/client in my nextjs application (only frontend). My backend is done using nestjs-graphql. I am facing one problem using #apollo/client.
Here is my code-
client.ts
//Initialize apolloClient
let apolloClient: ApolloClient<NormalizedCacheObject>;
//Creating link
function createIsomorphLink() {
return new HttpLink({
uri: "http://localhost:5000/piechat",
credentials: 'same-origin'
})
}
//Create apollo client
function createApolloClient() {
return new ApolloClient({
ssrMode: true,
link: createIsomorphLink(),
cache: new InMemoryCache()
})
}
//Initialize Apollo
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
if (initialState) {
const existingCache = _apolloClient.extract()
const data = merge(initialState, existingCache)
_apolloClient.cache.restore(data)
}
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
//Use Apollo
export function useApollo(initialState = null) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
Then in nextjs getServerSideProps-
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const apolloClient = initializeApollo()
await apolloClient.query({
query: GET_CONVERSATION_LIST,
context: { headers: { authorization: `Bearer ${ctx.req.cookies["session"]}` } },
errorPolicy: "all"
})
return {
props: {
initialApolloState: apolloClient.cache.extract(),
}
}
}
Then in one conversation.tsx(I use this data)-
const { data } = useQuery<GetConversationData>(GET_CONVERSATION_LIST);
On the other component (createConver.tsx) I use fetchMore function-
const { fetchMore } = useQuery(GET_CONVERSATION_LIST)
useEffect(() => {
if (data) { //If new data creation is success
fetchMore({
context: { headers: { authorization: `Bearer ${getCookie("session")}` } }, updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult
}
})
}
}, [data])
Everything is well. But when I refresh my page, I see that new data not comes. please see this gif- enter image description here
But I am not understanding what is the problem. I am not getting any docs or any things else. Please help me anyone.
I am try to use #apollo/client in my nextjs application (only frontend). My backend is done using nestjs-graphql. I am facing one problem using #apollo/client.
Here is my code-
client.ts
//Initialize apolloClient
let apolloClient: ApolloClient<NormalizedCacheObject>;
//Creating link
function createIsomorphLink() {
return new HttpLink({
uri: "http://localhost:5000/piechat",
credentials: 'same-origin'
})
}
//Create apollo client
function createApolloClient() {
return new ApolloClient({
ssrMode: true,
link: createIsomorphLink(),
cache: new InMemoryCache()
})
}
//Initialize Apollo
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
if (initialState) {
const existingCache = _apolloClient.extract()
const data = merge(initialState, existingCache)
_apolloClient.cache.restore(data)
}
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
//Use Apollo
export function useApollo(initialState = null) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
Then in nextjs getServerSideProps-
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const apolloClient = initializeApollo()
await apolloClient.query({
query: GET_CONVERSATION_LIST,
context: { headers: { authorization: `Bearer ${ctx.req.cookies["session"]}` } },
errorPolicy: "all"
})
return {
props: {
initialApolloState: apolloClient.cache.extract(),
}
}
}
Then in one conversation.tsx(I use this data)-
const { data } = useQuery<GetConversationData>(GET_CONVERSATION_LIST);
On the other component (createConver.tsx) I use fetchMore function-
const { fetchMore } = useQuery(GET_CONVERSATION_LIST)
useEffect(() => {
if (data) { //If new data creation is success
fetchMore({
context: { headers: { authorization: `Bearer ${getCookie("session")}` } }, updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult
}
})
}
}, [data])
Everything is well. But when I refresh my page, I see that new data not comes. please see this gif-
But I am not understanding what is the problem. I am not getting any docs or any things else. Please help me anyone.
So I'm trying to do a custom header for the requests to my graphql backend from my nextJS frontend. The code of the graphql client is as follows:
import { ApolloClient, ApolloLink, InMemoryCache, createHttpLink } from '#apollo/client'
import { setContext } from '#apollo/client/link/context'
import getConfig from 'next/config'
import nookies, { parseCookies } from 'nookies'
const { publicRuntimeConfig } = getConfig()
const httpLink = createHttpLink({
uri: publicRuntimeConfig.uri,
credentials: 'same-origin'
})
const authLink = setContext((_, { headers }) => {
let token = ''
if (typeof window !== 'undefined') {
const { 'toDo-token': newToken } = parseCookies()
token = newToken
} else {
// error is right here. I cant find a way to access the ctx
const { 'toDo-token': newToken } = nookies.get(ctx)
token = newToken
}
return {
headers: {
...headers,
Authorization: token ?? undefined
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
ssrMode: typeof window === 'undefined'
})
export default client
On the client side it works just fine. However, since I cant access client cookies on the server side, I'm trying to use the get function from nookies to grab the auth cookies and pass to the headers with the setContext function. The problem I'm having is that I cant find a way to grab the app context from next so I can look at the request info and grab the ctx from there so I can see the cookies.
I do something similar in my _app.tsx in the following way:
MyApp.getInitialProps = async ({ ctx }: AppContext) => {
const { locale } = ctx
const { 'toDo-token': token } = nookies.get(ctx)
let user = null
try {
user = await useUser()
} catch {
user = null
}
return {
locale: locale === 'default' ? 'en' : locale as LocaleEnum,
token: token ?? null,
user
}
}
Does anyone know how I can access the request context in the graphql setContext function?
I have a fresh copy of laravel with sanctum and lighthouse. When I do the login route via axios, everything works as expected. After logging in via axios, I added a lazyquery to attempt to query some guarded fields but I get unauthenticated. I am not sure why and it has been three days I've been dealing with this. I'd really appreciate your help.
This works
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log(`logged in ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", { withCredentials: true })
.then(function (resolve) {
console.log(`gated ${resolve.data}`);
axios
.get("http://api.newods.test/api/logout", {
withCredentials: true,
})
.then(function (resolve) {
console.log(`logged out ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", {
withCredentials: true,
})
.then(function (resolve) {
console.log(
`trying to get to gated after logging out ${resolve.data}`
);
});
});
});
});
});
}, []);
But when I cut it short and change to this, I get unauthenticated
const HELLO = gql\`
query hello {
hello
}
`;
function Home() {
const [hello, { loading, data }] = useLazyQuery(HELLO);
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log('logged in');
});
});
}, []);
return (
<div className="container">
<div>Index</div>
<button onClick={() => hello()}>
Click to hello world
</button>
<p>{data && data.hello || ''}</p>
</div>
);
}
export default withApollo(Home);
And that returns unauthenticated when I add the #guard directive and I see the token from the axios login request is in the headers... I am not sure what I am missing here I'd greatly appreciate your help.
schema.graphql
type Query {
users: [User!]! #paginate(defaultCount: 10)
user(id: ID #eq): User #find
hello: String! #guard
me: User #auth
}
.env
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_DOMAIN=.newods.test
SANCTUM_STATEFUL_DOMAINS=newods.test:3000
config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'graphql'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
config/lighthouse
'route' => [
/*
* The URI the endpoint responds to, e.g. mydomain.com/graphql.
*/
'uri' => '/graphql',
/*
* Lighthouse creates a named route for convenient URL generation and redirects.
*/
'name' => 'graphql',
/*
* Beware that middleware defined here runs before the GraphQL execution phase,
* make sure to return spec-compliant responses in case an error is thrown.
*/
'middleware' => [
\Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
// Logs in a user if they are authenticated. In contrast to Laravel's 'auth'
// middleware, this delegates auth and permission checks to the field level.
\Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class,
],
/*
* The `prefix` and `domain` configuration options are optional.
*/
//'prefix' => '',
//'domain' => '',
],
In my next app with apollo
create.js
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import Cookies from 'js-cookie';
import { serverUrl } from '../config';
export default function createApolloClient(initialState, ctx) {
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = Cookies.get("XSRF-TOKEN");
// console.log(`token is ${token}`);
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
"Access-Control-Allow-Credentials": true,
...(token ? { authorization: `X-XSRF-TOKEN=${token}` } : {}),
},
};
});
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'same-origin',
});
return new ApolloClient({
ssrMode: Boolean(ctx),
link: authLink.concat(httpLink),
connectToDevTools: true,
cache: new InMemoryCache().restore(initialState),
});
}
withApollo.js
import React from "react";
import Head from "next/head";
import { ApolloProvider } from "#apollo/react-hooks";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";
import createApolloClient from './create';
let apolloClient = null;
/**
* Creates and provides the apolloContext
* to a next.js PageTree. Use it by wrapping
* your PageComponent via HOC pattern.
* #param {Function|Class} PageComponent
* #param {Object} [config]
* #param {Boolean} [config.ssr=true]
*/
export function withApollo(PageComponent, { ssr = true } = {}) {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState);
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
);
};
// Set the correct displayName in development
if (process.env.NODE_ENV !== "production") {
const displayName =
PageComponent.displayName || PageComponent.name || "Component";
if (displayName === "App") {
console.warn("This withApollo HOC only works with PageComponents.");
}
WithApollo.displayName = `withApollo(${displayName})`;
}
if (ssr || PageComponent.getInitialProps) {
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx;
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(
{},
ctx.req.headers.cookie
));
// Run wrapped getInitialProps methods
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
}
// Only on the server:
if (typeof window === "undefined") {
// When redirecting, the response is finished.
// No point in continuing to render
if (ctx.res && ctx.res.finished) {
return pageProps;
}
// Only if ssr is enabled
if (ssr) {
try {
// Run all GraphQL queries
const { getDataFromTree } = await import("#apollo/react-ssr");
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient,
}}
/>
);
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
console.error("Error while running `getDataFromTree`", error);
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind();
}
}
// Extract query data from the Apollo store
// #ts-ignore
const apolloState = apolloClient.cache.extract();
return {
...pageProps,
apolloState,
};
};
}
return WithApollo;
}
/**
* Always creates a new apollo client on the server
* Creates or reuses apollo client in the browser.
* #param {Object} initialState
*/
function initApolloClient(initialState = {}, cookie = "") {
// Make sure to create a new client for every server-side request so that data
// isn"t shared between connections (which would be bad)
if (typeof window === "undefined") {
return createApolloClient(initialState, cookie);
}
// Reuse client on the client-side
if (!apolloClient) {
// #ts-ignore
apolloClient = createApolloClient(initialState);
}
return apolloClient;
}
I have a very similar architecture, but using Vue. From comparing your code against my working implementation, I think the majority of your problems are in create.js.
I don't know much about js-cookie, but this is how I get the XSRF-TOKEN, and decode it.
let token = RegExp('XSRF-TOKEN[^;]+').exec(document.cookie)
token = decodeURIComponent(token ? token.toString().replace(/^[^=]+./, '') : '')
Then, in your setContext, you need to set the header as follows.
return {
headers: {
...headers,
'X-XSRF-TOKEN': token,
}
}
Also, I had trouble with credentials: 'same-origin' even though I'm using a subdomain. Therefore I would suggest:
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'include',
})
Previously my apollo setup was listening to subscriptions until I added in socket.io and now my client setup is no longer listening to new data. my server codes seem to be ok based on my testing using graphql playground.
in my browser console, i get the following error message
client.js:652 WebSocket connection to 'ws://localhost:4000/' failed: Error during WebSocket handshake: Unexpected response code: 400
There seems to be some issue with my client side setup to use apollo subscriptions.
Appreciate any pointers? Thanks in advance
import { ApolloClient } from "apollo-client";
import { onError } from "apollo-link-error";
import { ApolloLink, split } from "apollo-link";
import { createUploadLink } from "apollo-upload-client";
import gql from "graphql-tag";
import { withClientState } from "apollo-link-state";
import { InMemoryCache } from "apollo-cache-inmemory";
import { WebSocketLink } from "apollo-link-ws";
import { getMainDefinition } from "apollo-utilities";
import { setContext } from "apollo-link-context";
const cache = new InMemoryCache();
const defaultState = {
currentGame: {
__typename: "currentGame",
teamAScore: 0,
teamBScore: 0,
teamAName: "EAGLES",
teamBName: "LOL"
}
};
const stateLink = withClientState({
cache,
defaults: defaultState,
resolvers: {
Mutation: {
updateGame: (_, { index, value }, { cache }) => {
const query = gql`
query GetCurrentGame {
currentGame #client {
teamAScore
teamBScore
teamAName
teamBName
}
}
`;
const previous = cache.readQuery({ query });
const data = {
currentGame: {
...previous.currentGame,
[index]: value
}
};
cache.writeQuery({ query, data });
return null;
},
resetCurrentGame: (_, d, { cache }) => {
cache.writeData({ data: defaultState });
}
}
}
});
const host = "http://localhost:4000";
// httpLink
const httpLink = createUploadLink({
uri: `${host}/graphql`,
credentials: "same-origin"
});
// wsLink
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/`,
options: {
reconnect: true
}
});
// using the ability to split links, you can send data to each link
// depending on what kind of operation is being sent
const webLink = split(
// split based on operation type
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return kind === "OperationDefinition" && operation === "subscription";
},
wsLink,
httpLink
);
// authMiddleware
const authLink = setContext(async (req, { headers }) => {
// const token = await AsyncStorage.getItem("#token");
const token = "";
return {
headers: {
...headers,
authorization: token ? `${token}` : ""
}
};
});
const errorLink = onError(({ networkError, graphQLErrors }) => {
if (graphQLErrors) {
graphQLErrors.map(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
}
if (networkError) console.log(`[Network error]: ${networkError}`);
});
export const client = new ApolloClient({
link: ApolloLink.from([authLink, stateLink, errorLink, webLink]),
cache
});
My server side code if required
//! Using Apollo Server Express
const app = express();
const path = "/graphql";
const schema = genSchema();
export const startServer = async () => {
const server = new ApolloServer({
schema,
context: ({ req }: any) => ({
req,
pubsub,
userLoader: userLoader()
})
});
app.use(cors());
app.use(authMiddleware);
app.use("/images", express.static("images"));
app.use(
"graphql",
graphqlUploadExpress({
uploadDir: "/",
maxFileSize: 100000000,
maxFiles: 10
}),
graphqlHTTP({ schema }) as any
);
server.applyMiddleware({ app, path });
//! Added Subscription Handler
const httpServer = createServer(app);
server.installSubscriptionHandlers(httpServer);
const port = process.env.PORT || 4000;
await createConnection();
await httpServer.listen({
port
});
console.log(`Server is running on localhost:${port}${server.graphqlPath}`);
};
Rectified. My client side apollo setup should point to ws://localhost:4000/graphql and not just ws://localhost:4000/
// wsLink
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000/graphql`,
options: {
reconnect: true
}
});