The solution to this question is muddled in my head, so if I’m attacking this in entirely the wrong way, please let me know.
The Goal:
An infinite scrolling list with polled updates. (I can’t use subscriptions because the server framework I’m using hasn’t yet implemented it). Essentially the container paginates the past and polls the future with a timestamp.
The Problem:
I’ve tried several implementations, but I’m not sure how to pass the props correctly. Starting with my query:
const query = gql`
query messages($threadId: Int!, $first: Int!, $after: String, $timestamp: String){
thread(threadId: $threadId) {
id, subject
messages(first: $first, after: $after, timestamp: $timestamp) {
pageInfo {
endCursor
hasNextPage
}
edges {
node {
id
body
isSent
insertedAt
updatedAt
user {
name
avatar
}
}
}
}
}
}
`;
Paginated queries do not send a timestamp. Polled queries do. The server returns the correct messages in GraphiQL.
The Apollo Store, via Apollo Dev Tools, updates its store correctly. For instance, if the first paginated query returns messages with ids of 1 and 2, and 20 seconds later the polled query returns a new message with id: 3, it’s added. Or if the message from id: 2 is edited, the Apollo store updates it.
The downstream view components receive props from the query. That means I need to handle the query merging manually. That seems like a lot of silly work. But I’m not sure how to keep these queries while passing the updated store props.
I’ve gone through the store and racked my brain out. Any ideas are appreciated.
Here is my config object. loadMoreRows
is returned by the infinite scrolling component when it wants more rows and pollForNewMessages
is called by the container component on a setInterval with a timestamp based on the container’s local state.
const configObject = {
options: (props) => {
const after = props.endCursor || null;
return {
variables: { first, after, threadId: props.threadId }
};
},
force: true,
props: ({ ownProps, data }) => {
const { loading, fetchMore, thread } = data;
const { threadId } = ownProps;
const pollForNewMessages = ({ timestamp }) => {
return fetchMore({
variables: {
first: 1000, after: null, threadId, timestamp
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const newEdges = fetchMoreResult.thread.messages.edges;
return {
...previousResult,
thread: {
...previousResult.thread,
messages: {
edges: merge(previousResult.thread.messages.edges, newEdges, 'id'),
...previousResult.thread.messages.pageInfo
}
}
};
}
});
};
const loadMoreRows = () => {
return fetchMore({
variables: {
after: thread.messages.pageInfo.endCursor || null,
},
updateQuery: (previousResult, { fetchMoreResult }) => {
const newEdges = fetchMoreResult.thread.messages.edges;
const pageInfo = fetchMoreResult.thread.messages.pageInfo;
return {
...previousResult,
thread: {
...previousResult.thread,
messages: {
edges: merge(previousResult.thread.messages.edges, newEdges, 'id'),
pageInfo
}
}
};
}
});
};
return {
loading,
thread,
loadMoreRows,
pollForNewMessages
};
}
};
function merge(a, b, key) {
function x(a) {
a.forEach(function (b) {
if (!(b[key] in obj)) {
obj[b[key]] = obj[b[key]] || {};
array.push(obj[b[key]]);
}
Object.keys(b).forEach(function (k) {
obj[b[key]][k] = b[k];
});
});
}
var array = [],
obj = {};
x(a);
x(b);
return array;
}
export default compose(
graphql(query, configObject)
)(MessagesContainer);