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.