I'm looking for a solution where I can add the subscription to a specific route instead of binding the subscription globally when the app start.
I know the subscription can be acheived with following code
const wsLink = new WebSocketLink({
uri: `ws://localhost:4000`,
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem(AUTH_TOKEN),
}
}
})
const link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
authLink.concat(httpLink)
)
const client = new ApolloClient({
link,
cache: new InMemoryCache()
})
But if I add this, the subscription is directly active upon page load irrespective of what page I'm on.
Is there any solution where I can bind the subscription on a specific page instead of having it binded globally.
If you are passing token in a query parameter and want to reconnect to the link all you need to do is update the link back by reassigning it
wsLink.subscriptionClient.url = `${GraphQLSocketURL}/query?authorization=${newAccessToken}`;
And if you want to establish connection when the page is loaded all you need to do is set lazy flag to true
const wsLink = new WebSocketLink({
uri: `${GraphQLSocketURL}/query?authorization=${accessToken}`,
options: {
reconnect: true,
reconnectionAttempts: 10,
lazy: true,
},
});
Related
In our React application we store the currentUser in a state object that can change whenever the token is refreshed and is then passed in the header for API requests.
However because state changes in React cause the component tree to re-render it seems this also causes Apollo to re-fetch queries (even though these don't run on render but explicitly called via events (the events themselves are not re-called)).
So our Provider looks like this:
// ...rest of code removed for brevity...
// currentUser is a state object in a parent Context
const { currentUser } = useContext(AuthContext)
// ...rest of code removed for brevity...
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: currentUser
? `Bearer ${currentUser.authenticationToken}`
: ''
}
}
})
const client = useMemo(
() =>
new ApolloClient({
link: from([authLink, errorLink, httpLink]),
cache: new InMemoryCache({
dataIdFromObject: () => uuid.v4()
})
}),
[authLink, errorLink, httpLink]
)
return <ApolloProvider client={client}>{children}</ApolloProvider>
}
export default CustomApolloProvider
And then in some lower level component:
// This query will run after every currentUser change
// AFTER the first time this query is run via the click event
const [
loadData,
{ loading: loadingData, error: errorData }
] = useLazyQuery(DATA_QUERY, {
onCompleted: (data) => {
console.log('data', data)
}
})
<Button onClick={() => loadData()}>Load Data</Button>
So after clicking that Button to load some data, and then making the currentUser update, that query will then re-fetch... even though the query should ONLY be firing on event click...
Our current solution is to drop any use of 'states' inside this component and instead pull values directly from either a service or from localStorage for that header, so there's no chance of a re-fetch on the state change which works...
But this seems really flaky... is there a way to stop Apollo re-fetching these queries if a state changes inside its Provider?
I have an ApolloClient performing a query through useQuery.
I'd like that object to be available to the whole app, without having to re-fetch it from the database. So I was implementing useContext.
However, i remembered that ApolloClient caches its results.
So if I have this line of code:
const { loading, error, data } = useQuery(USER);
and I repeat that line of code across multiple components, will the components further down the tree just access the cached version? Basically negating the need for useContext?
Thank you.
here's my client config:
const client = new ApolloClient({
link: new HttpLink({
uri: graphql_url,
fetch: async (uri: string, options: any) => {
options.headers.Authorization = `Bearer ${realmUserToken}`;
return fetch(uri, options);
},
}),
cache: new InMemoryCache(),
});
I'm writing an app that uses Apollo Client to make graphql requests against a MongoDB Realm database.
This is the abstract structure of it:
<MongoContext>
<ApolloContext>
<App/>
</ApolloContext>
</MongoContext>
The top level component handles user authentication and provides a context. The next component down initiates Apollo Client and the caching logic and sets the context to the whole app.
The expected data flow is shown in the diagram on this page. The default behavior for a useQuery is that Apollo:
Tries to fetch from cache;
Tries to fetch from server; If successful, saves to cache.
My goal is to achieve offline capability. So, referring to the diagram again, the very first query should be resolved by the cache when it holds the data. Apollo's default cache mechanism is in memory, so I'm using apollo-cache-persist to cache it to localStorage.
More specifically, these are the conditions required:
The app should be responsive upon start.
At first render, the app is either offline or hasn't authenticated yet
Therefore it must read from cache, if available
If there's no cache, don't make any requests to the server (they'd all fail)
If the user is online, the app should get the authentication token for the requests
The token is requested asynchronously
While the token is unknown, read from the cache only (As 1.2 above)
When the token is known, use the data flow described above
My main problems are specifically with 1.2 and 2.2 above. I.e. preventing Apollo from making requests to the server when we already know it will fail.
I was also looking for a global solution, so modifying individual queries with skip or useLazyQuery aren't options. (And I'm not even sure that would work - I still needed the queries to be executed against the cache.)
Code:
ApolloContext component:
import * as React from 'react';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
NormalizedCacheObject,
} from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { persistCache } from 'apollo-cache-persist';
import { PersistentStorage } from 'apollo-cache-persist/types';
const ApolloContext: React.FC = ({ children }) => {
// this hook gets me the token asynchronously
// token is '' initially but eventually resolves... or not
const { token } = useToken();
const cache = new InMemoryCache();
const [client, setClient] = React.useState(createApolloClient(token, cache))
// first initialize the client without the token, then again upon receiving it
React.useEffect(() => {
const initPersistCache = async () => {
await persistCache({
cache,
storage: capacitorStorageMethods,
debug: true,
});
};
const initApollo = async () => {
await initPersistCache();
setClient(createApolloClient(token, cache));
};
if (token) {
initApollo();
} else {
initPersistCache();
}
}, [token]);
console.log('id user', id, user);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
function createApolloClient(
token: string,
cache: InMemoryCache
) {
const graphql_url = `https://realm.mongodb.com/api/client/v2.0/app/${realmAppId}/graphql`;
const httpLink = createHttpLink({
uri: graphql_url,
});
const authorizationHeaderLink = setContext(async (_, { headers }) => {
return {
headers: {
...headers,
Authorization: `Bearer ${token}`,
},
};
});
return new ApolloClient({
link: authorizationHeaderLink.concat(httpLink),
cache,
});
}
What I've tried:
After attempting many different things. I found something that works, but it looks terrible. The trick is to give Apollo a custom fetch that rejects all requests when the user is not logged in:
const customFetch = (input: RequestInfo, init?: RequestInit | undefined) => {
return user.isLoggedIn
? fetch(input, init)
: Promise.reject(new Response());
};
const httpLink = createHttpLink({
uri: graphql_url,
fetch: customFetch,
});
Another way to prevent outbound requests is to just omit the link property:
return new ApolloClient({
link: user.isLoggedIn
? authorizationHeaderLink.concat(httpLink)
: undefined,
cache,
});
}
That looks way cleaner but now the problem is that make queries that can't be fulfilled by the cache to hang on loading forever.(related issue)
I'm looking for a cleaner and safer way to do this.
Let's suppose we have such a schema on the client.
const typeDefs = gql`
type System {
showSignInModal: Boolean!
menuPermanent: Boolean!
isLogged: Boolean!
accessToken: String
themeMode: String
}
`;
How can we create a mutation to change the menuPermanent value to true? < br />
In Apollo v3 local field resolvers had been deprecated so now we can't do the standard #client mutation.
In the Apollo v3 documentation, I saw they suggest to use direct client.writeQuery or client.writeFragment methods but I don't understand clearly how can they be used. Does it mean we don't need to create a Mutation to change the cache but rather import the client Instance or receive it by using useApolloClient hook and execute the writeQuery method directly on it?
Reactive variables are nice but I'm interested in the same way for all app parts. Either server or client. Previously both of them used Mutations but now I'm a bit confused with all of these major changes so I'd really like to know how can we use Mutations in the case of Apollo v3 to update the local cache state property.
Apollo client initialization
const typeDefs = gql`
type System {
showSignInModal: Boolean!
menuPermanent: Boolean!
isLogged: Boolean!
accessToken: String
themeMode: String
}
`;
export let cache = new InMemoryCache();
// SET THE INITIAL LOCAL CACHE STATE
cache.writeQuery({
query: GET_SYSTEM_DATA,
data: {
system: {
showSignInModal: false,
menuPermanent: false,
isLogged: false,
accessToken: null,
themeMode: 'white'
}
}
});
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: new HttpLink({
uri: 'https://nextjs-graphql-with-prisma-simple.vercel.app/api', // Server URL (must be absolute)
credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers`
}),
typeDefs,
cache: cache
})
}
The component where I'm trying to change the cache value
import { gql, useApolloClient } from '#apollo/client';
const B = () => {
const client = useApolloClient();
return (
<div>
<span>Test Query Component B</span>
<button onClick={() => {
client.writeFragment({
fragment: gql`
fragment MySystem on System {
menuPermanent
}
`,
data: {
system: { menuPermanent: true, }
},
})
}} >Set menu permanent</button>
</div>
)
}
But it doesn't change the local cache at all.
Thanks for any help or info.
I've followed Apollo's docs for setting up GraphQL subscriptions on both the client and server, and though I'm 90% there, I can't figure out how to set up subscription channels and how to connect mutations to those channels so that whenever a mutation occurs, the server pushes the new data to the client. (For content, I'm making a Reddit clone where people post topics and others comment on it. So when you see "Topics" or "TopicList," think of those as posts.)
So far, I have set up Apollo Client for subscriptions successfully:
const wsClient = new SubscriptionClient('ws://localhost:3001/subscriptions', {
reconnect: true
});
const networkInterface = createNetworkInterface({
uri: '/graphql',
opts: {
credentials: 'same-origin'
}
});
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient,
);
const client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
dataIdFromObject: o => o.id
});
And I've set up my back-end for subscriptions as well. Here's my server.js file:
//===========================================================
//Subscription Managaer
//===========================================================
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({
schema: schema,
pubsub: pubsub
});
//=====================================
//WebSocket + Express Server
//=====================================
const server = createServer(app);
//setup listening port
server.listen(3001, ()=>{
new SubscriptionServer(
{
subscriptionManager: subscriptionManager,
onConnect: (connectionParams, webSocket) => {
console.log('Websocket connection established');
},
onSubscribe: (message, params, webSocket) => {
console.log("The client has been subscribed", message, params);
},
onUnsubsribe: (webSocket) => {
console.log("Now unsubscribed");
},
onDisconnect: (webSocket) => {
console.log('Now disconnected');
}
},
{
server: server,
path: '/subscriptions',
});
console.log('Server is hot my man!');
})
I know these are successful, because I get the "Websocket connection established" message logged in my terminal.
Next is the actual subscription - I've created a subscription schema type (just like queries and mutations):
const SubscriptionType = new GraphQLObjectType({
name: 'Subscription',
fields: () => ({
topicAdded: {
type: TopicType,
args: {repoFullName: {type: GraphQLString}}, //I don't understand what repoFullName is - I was trying to follow the Apollo docs, but they never specified that
resolve(parentValue, args){
return parentValue;
}
}
})
});
module.exports = SubscriptionType;
and incorporated it into my root schema. So when I check out GraphiQL, I see: this subscription available in the Docs side menu
My GraphiQIL UI showing the subscriptionSchema successfully
In my React component, I successfully 'subscribe' to it using Apollo's subscribeToMore method:
const TOPICS_SUBSCRIPTION = gql`
subscription OnTopicAdded($repoFullName: String){
topicAdded(repoFullName: $repoFullName){
id
}
}
`;
class TopicList extends Component {
componentDidMount() {
this.createMessageSubscription = this.props.data.subscribeToMore({
document: TOPICS_SUBSCRIPTION,
// updateQuery: (previousState, {subscriptionData}) => {
// const newTopic = subscriptionData.data.Topic.node
// const topics = previousState.findTopics.concat([newTopic])
// return {
// findTopics: topics
// }
// },
onError: (err) => console.error(err)
})
} //...
And I get my "The client has been subscribed" message logged into my terminal. But this is where I'm stuck. I've read about the SetupFunction for the SubscriptionManager, but that isn't included in Apollo's docs. And I can't find how to map a 'createTopic' mutation to this subscription so that whenever someone adds a new topic, it pops up in TopicList.
I realize this is really long, but I've been pulling my hair out tyring to figure out what's the next step. Any help would be much appreciated!! Thank you for reading!
Yes you are missing the setup function. You could take a look at this links GraphQL subscription docu or this example.
How it should work:
First you need the channel on which you publish the changed data. In your case it could look like this:
const manager = new sub.SubscriptionManager({
schema,
pubSub,
setupFunctions: {
topicAdded: (options, args) => ({ // name of your graphQL subscription
topicAddedChannel: { // name of your pubsub publish-tag
filter: (topic) => {
console.log(topic); //should always show you the new topic if there is a subscribed client
return true; // You might want to add a filter. Maybe for guest users or so
},
},
}),
},
});
And here you see the need of the args: {repoFullName: {type: GraphQLString}} argument in the subscription. If you want to filter the subscription dependent on the "repoName". Meaning that only the client with a subscription with the "repoName" as argument gets an update.
Next you need a place where you call the pubsub.publish function. In your case after the add topic mutation has passed. Could look like this:
...
const topic = new Topic(/* args */);
topic.save((error, topic) => {
if (!error) {
pubsub.publish("topicAddedChannel", topic);
}
...
});
....