I have been searching for days to find the answer to this problem and I can’t find any mention of it anywhere.
I have a React application that uses both Queries and Subscriptions.
Queries are hitting /api
Subscriptions are hitting /api/subscriptions
The flow is 1 Query for filter options to put in variables, 3 Queries for initial data, then 3 Subscriptions for updates to that data. Let’s call them People, Books, and Cars.
Query Filters /api
…then
Query People /api
Query Books /api
Query Cars /api
…then
Subscribe People /subscriptions
Subscribe Books /subscriptions
Subscribe Cars /subscriptions
The problem is that the last Subscribe (in this case Cars) is the only one that will receive updates. If i change the order to Cars, People, Books, then Books receives the updates.
Checking Chrome dev tools I can see the messages all have different id
s under the /subscriptions
endpoint.
The server is is managed by a different team and it’s written in Go with the github.com/gorilla/websocket package. I THINK that is due to no Apollo server package available for Go. Not sure if this is a client side or server side issue
Here is an example of the client side implementation
Apollo setup
import { ApolloClient, ApolloProvider, split, HttpLink, InMemoryCache } from '@apollo/client';
import { getMainDefinition } from '@apollo/client/utilities';
import { WebSocketLink } from '@apollo/client/link/ws';
const httpLink = new HttpLink({
uri: process.env.REACT_APP_SERVICE_API,
credentials: 'include'
});
const wsLink = new WebSocketLink({
uri: `wss://${process.env.REACT_APP_SERVICE_API.replace('https://', '')}/subscriptions`,
credentials: 'include',
options: {
reconnect: true,
wsOptionArguments: []
}
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache
});
Component
import { useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { FILTERS_QUERY } from '../../graphql/filters.query';
import { PEOPLE_SUBSCRIPTION, PEOPLE_QUERY } from '../../graphql/people.operation';
import { BOOKS_SUBSCRIPTION, BOOKS_QUERY } from '../../graphql/books.operation';
import { CARS_SUBSCRIPTION, CARS_QUERY } from '../../graphql/cars.operation';
import { FilterBar, FilterChip, LoadingSpinner } from '../';
import { useLayoutContext, useStateCallback } from '../../hooks';
import { isNotEmpty, Logger } from '../../utils';
import OverviewSections from './OverviewSections/OverviewSections';
const Overview = () => {
const { isLoading, setIsLoading } = useLayoutContext();
const [filters, setFilters] = useStateCallback({ people: [] });
const { loading: filtersLoading, data: filtersData } = useQuery(FILTERS_QUERY, {
onError: (error) => Logger('filtersError: ', error.message),
onCompleted: (data) => {
setFilters({
people: data.filters.people
});
}
});
const { data: peopleData, loading: peopleLoading, subscribeToMore: subscribeToMorePeople } = useQuery(PEOPLE_QUERY, {
variables: filters,
skip: !isNotEmpty(filters.people),
errorPolicy: 'all',
onCompleted: (data) => Logger('peopleData', data),
onError: (error) => Logger('peopleError', error.message)
});
useEffect(() => {
if (isNotEmpty(filters.people)) {
subscribeToMorePeople({
document: PEOPLE_SUBSCRIPTION,
variables: filters,
updateQuery: (prev, { subscriptionData }) => {
Logger('PEOPLE_SUBSCRIPTION: ', subscriptionData);
if (!subscriptionData.data) {
return prev;
}
return Object.assign({}, prev, {
people: subscriptionData.data.people
});
}
});
}
}, [subscribeToMorePeople, filters]);
const { data: booksData, loading: booksLoading, subscribeToMore: subscribeToMoreBooks } = useQuery(BOOKS_QUERY, {
variables: filters,
skip: !isNotEmpty(filters.people),
errorPolicy: 'all',
onCompleted: (data) => Logger('booksData', data),
onError: (error) => Logger('booksError', error.message)
});
useEffect(() => {
if (isNotEmpty(filters.people)) {
subscribeToMoreBooks({
document: BOOKS_SUBSCRIPTION,
variables: filters,
updateQuery: (prev, { subscriptionData }) => {
Logger('BOOKS_SUBSCRIPTION', subscriptionData);
if (!subscriptionData.data) {
return prev;
}
return Object.assign({}, prev, {
books: subscriptionData.data.books
});
}
});
}
}, [subscribeToMoreBooks, filters]);
const { data: carsData, loading: carsLoading, subscribeToMore: subscribeToMoreCars } = useQuery(CARS_QUERY, {
variables: filters,
skip: !isNotEmpty(filters.people),
errorPolicy: 'all',
onCompleted: (data) => Logger('carsData', data),
onError: (error) => Logger('carsError', error.message)
});
useEffect(() => {
if (isNotEmpty(filters.people)) {
subscribeToMoreCars({
document: CARS_SUBSCRIPTION,
variables: filters,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) {
return prev;
}
if (isNotEmpty(prev.cars)) {
return Object.assign({}, prev, {
cars: [subscriptionData.data.cars, ...prev.cars]
});
} else {
return {
cars: [subscriptionData.data.cars]
};
}
}
});
}
}, [filters, subscribeToMoreCars]);
useEffect(() => {
setIsLoading([ filtersLoading, booksLoading, peopleLoading, carsLoading].includes(true));
}, [setIsLoading, filtersLoading, booksLoading, peopleLoading, carsLoading]);
function handleApply(list, filterKey) {
setFilters((prevState) => ({
...prevState,
[filterKey]: list
}));
}
return (
<div id="overview">
<section className="top-action-bar">
{isNotEmpty(filtersData) && (
<FilterBar>
<FilterChip
label="People"
id="overview-people-filter"
data={filtersData.filters.people}
filterKey="people"
onApply={handleApply}
/>
</FilterBar>
)}
</section>
<OverviewSections
books={booksData?.books}
cars={carsData?.cars}
people={peopleData?.people}
filters={filtersData?.filters}
/>
<LoadingSpinner show={isLoading} delay={1000} />
</div>
);
};
export default Overview;