Im using update after a mutation to update the store when a new comment is created. I also have a subscription for comments on this page.
Either one of these methods works as expected by itself. However when I have both, then the user who created the comment will see the comment on the page twice and get this error from React:
Warning: Encountered two children with the same key,
I think the reason for this is the mutation update and the subscription both return a new node, creating a duplicate entry. Is there a recommended solution to this? I couldn’t see anything in the Apollo docs but it doesn’t seem like that much of an edge use case to me.
This is the component with my subscription:
import React from 'react';
import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
import Comments from './Comments';
import NewComment from './NewComment';
import _cloneDeep from 'lodash/cloneDeep';
import Loading from '../Loading/Loading';
class CommentsEventContainer extends React.Component {
_subscribeToNewComments = () => {
this.props.COMMENTS.subscribeToMore({
variables: {
eventId: this.props.eventId,
},
document: gql`
subscription newPosts($eventId: ID!) {
Post(
filter: {
mutation_in: [CREATED]
node: { event: { id: $eventId } }
}
) {
node {
id
body
createdAt
event {
id
}
author {
id
}
}
}
}
`,
updateQuery: (previous, { subscriptionData }) => {
// Make vars from the new subscription data
const {
author,
body,
id,
__typename,
createdAt,
event,
} = subscriptionData.data.Post.node;
// Clone store
let newPosts = _cloneDeep(previous);
// Add sub data to cloned store
newPosts.allPosts.unshift({
author,
body,
id,
__typename,
createdAt,
event,
});
// Return new store obj
return newPosts;
},
});
};
_subscribeToNewReplies = () => {
this.props.COMMENT_REPLIES.subscribeToMore({
variables: {
eventId: this.props.eventId,
},
document: gql`
subscription newPostReplys($eventId: ID!) {
PostReply(
filter: {
mutation_in: [CREATED]
node: { replyTo: { event: { id: $eventId } } }
}
) {
node {
id
replyTo {
id
}
body
createdAt
author {
id
}
}
}
}
`,
updateQuery: (previous, { subscriptionData }) => {
// Make vars from the new subscription data
const {
author,
body,
id,
__typename,
createdAt,
replyTo,
} = subscriptionData.data.PostReply.node;
// Clone store
let newPostReplies = _cloneDeep(previous);
// Add sub data to cloned store
newPostReplies.allPostReplies.unshift({
author,
body,
id,
__typename,
createdAt,
replyTo,
});
// Return new store obj
return newPostReplies;
},
});
};
componentDidMount() {
this._subscribeToNewComments();
this._subscribeToNewReplies();
}
render() {
if (this.props.COMMENTS.loading || this.props.COMMENT_REPLIES.loading) {
return <Loading />;
}
const { eventId } = this.props;
const comments = this.props.COMMENTS.allPosts;
const replies = this.props.COMMENT_REPLIES.allPostReplies;
const { user } = this.props.COMMENTS;
const hideNewCommentForm = () => {
if (this.props.hideNewCommentForm === true) return true;
if (!user) return true;
return false;
};
return (
<React.Fragment>
{!hideNewCommentForm() && (
<NewComment
eventId={eventId}
groupOrEvent="event"
queryToUpdate={COMMENTS}
/>
)}
<Comments
comments={comments}
replies={replies}
queryToUpdate={{ COMMENT_REPLIES, eventId }}
hideNewCommentForm={hideNewCommentForm()}
/>
</React.Fragment>
);
}
}
const COMMENTS = gql`
query allPosts($eventId: ID!) {
user {
id
}
allPosts(filter: { event: { id: $eventId } }, orderBy: createdAt_DESC) {
id
body
createdAt
author {
id
}
event {
id
}
}
}
`;
const COMMENT_REPLIES = gql`
query allPostReplies($eventId: ID!) {
allPostReplies(
filter: { replyTo: { event: { id: $eventId } } }
orderBy: createdAt_DESC
) {
id
replyTo {
id
}
body
createdAt
author {
id
}
}
}
`;
const CommentsEventContainerExport = compose(
graphql(COMMENTS, {
name: 'COMMENTS',
}),
graphql(COMMENT_REPLIES, {
name: 'COMMENT_REPLIES',
}),
)(CommentsEventContainer);
export default CommentsEventContainerExport;
And here is the NewComment component:
import React from 'react';
import { compose, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import './NewComment.css';
import UserPic from '../UserPic/UserPic';
import Loading from '../Loading/Loading';
class NewComment extends React.Component {
constructor(props) {
super(props);
this.state = {
body: '',
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
handleChange(e) {
this.setState({ body: e.target.value });
}
onKeyDown(e) {
if (e.keyCode === 13) {
e.preventDefault();
this.handleSubmit();
}
}
handleSubmit(e) {
if (e !== undefined) {
e.preventDefault();
}
const { groupOrEvent } = this.props;
const authorId = this.props.USER.user.id;
const { body } = this.state;
const { queryToUpdate } = this.props;
const fakeId = '-' + Math.random().toString();
const fakeTime = new Date();
if (groupOrEvent === 'group') {
const { locationId, groupId } = this.props;
this.props.CREATE_GROUP_COMMENT({
variables: {
locationId,
groupId,
body,
authorId,
},
optimisticResponse: {
__typename: 'Mutation',
createPost: {
__typename: 'Post',
id: fakeId,
body,
createdAt: fakeTime,
reply: null,
event: null,
group: {
__typename: 'Group',
id: groupId,
},
location: {
__typename: 'Location',
id: locationId,
},
author: {
__typename: 'User',
id: authorId,
},
},
},
update: (proxy, { data: { createPost } }) => {
const data = proxy.readQuery({
query: queryToUpdate,
variables: {
groupId,
locationId,
},
});
data.allPosts.unshift(createPost);
proxy.writeQuery({
query: queryToUpdate,
variables: {
groupId,
locationId,
},
data,
});
},
});
} else if (groupOrEvent === 'event') {
const { eventId } = this.props;
this.props.CREATE_EVENT_COMMENT({
variables: {
eventId,
body,
authorId,
},
optimisticResponse: {
__typename: 'Mutation',
createPost: {
__typename: 'Post',
id: fakeId,
body,
createdAt: fakeTime,
reply: null,
event: {
__typename: 'Event',
id: eventId,
},
author: {
__typename: 'User',
id: authorId,
},
},
},
update: (proxy, { data: { createPost } }) => {
const data = proxy.readQuery({
query: queryToUpdate,
variables: { eventId },
});
data.allPosts.unshift(createPost);
proxy.writeQuery({
query: queryToUpdate,
variables: { eventId },
data,
});
},
});
}
this.setState({ body: '' });
}
render() {
if (this.props.USER.loading) return <Loading />;
return (
<form
onSubmit={this.handleSubmit}
className="NewComment NewComment--initial section section--padded"
>
<UserPic userId={this.props.USER.user.id} />
<textarea
value={this.state.body}
onChange={this.handleChange}
onKeyDown={this.onKeyDown}
rows="3"
/>
<button className="btnIcon" type="submit">
Submit
</button>
</form>
);
}
}
const USER = gql`
query USER {
user {
id
}
}
`;
const CREATE_GROUP_COMMENT = gql`
mutation CREATE_GROUP_COMMENT(
$body: String!
$authorId: ID!
$locationId: ID!
$groupId: ID!
) {
createPost(
body: $body
authorId: $authorId
locationId: $locationId
groupId: $groupId
) {
id
body
author {
id
}
createdAt
event {
id
}
group {
id
}
location {
id
}
reply {
id
replyTo {
id
}
}
}
}
`;
const CREATE_EVENT_COMMENT = gql`
mutation CREATE_EVENT_COMMENT($body: String!, $eventId: ID!, $authorId: ID!) {
createPost(body: $body, authorId: $authorId, eventId: $eventId) {
id
body
author {
id
}
createdAt
event {
id
}
}
}
`;
const NewCommentExport = compose(
graphql(CREATE_GROUP_COMMENT, {
name: 'CREATE_GROUP_COMMENT',
}),
graphql(CREATE_EVENT_COMMENT, {
name: 'CREATE_EVENT_COMMENT',
}),
graphql(USER, {
name: 'USER',
}),
)(NewComment);
export default NewCommentExport;
And the full error message is:
Warning: Encountered two children with the same key, `cjexujn8hkh5x0192cu27h94k`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
in ul (at Comments.js:9)
in Comments (at CommentsEventContainer.js:157)
in CommentsEventContainer (created by Apollo(CommentsEventContainer))
in Apollo(CommentsEventContainer) (created by Apollo(Apollo(CommentsEventContainer)))
in Apollo(Apollo(CommentsEventContainer)) (at EventPage.js:110)
in section (at EventPage.js:109)
in DocumentTitle (created by SideEffect(DocumentTitle))
in SideEffect(DocumentTitle) (at EventPage.js:51)
in EventPage (created by Apollo(EventPage))
in Apollo(EventPage) (at App.js:176)
in Route (at App.js:171)
in Switch (at App.js:94)
in div (at App.js:93)
in main (at App.js:80)
in Router (created by BrowserRouter)
in BrowserRouter (at App.js:72)
in App (created by Apollo(App))
in Apollo(App) (at index.js:90)
in QueryRecyclerProvider (created by ApolloProvider)
in ApolloProvider (at index.js:89)
This is actually pretty easy to fix. I was confused for a long time as my subscriptions would intermittently fail. It turns out this was a Graphcool issue, switching from the Asian to the USA cluster stoped the flakiness.
You just have to test to see if the ID already exists in the store, and not add it if it does. Ive added code comments where I've changed the code:
_subscribeToNewComments = () => {
this.props.COMMENTS.subscribeToMore({
variables: {
eventId: this.props.eventId,
},
document: gql`
subscription newPosts($eventId: ID!) {
Post(
filter: {
mutation_in: [CREATED]
node: { event: { id: $eventId } }
}
) {
node {
id
body
createdAt
event {
id
}
author {
id
}
}
}
}
`,
updateQuery: (previous, { subscriptionData }) => {
const {
author,
body,
id,
__typename,
createdAt,
event,
} = subscriptionData.data.Post.node;
let newPosts = _cloneDeep(previous);
// Test to see if item is already in the store
const idAlreadyExists =
newPosts.allPosts.filter(item => {
return item.id === id;
}).length > 0;
// Only add it if it isn't already there
if (!idAlreadyExists) {
newPosts.allPosts.unshift({
author,
body,
id,
__typename,
createdAt,
event,
});
return newPosts;
}
},
});
};
_subscribeToNewReplies = () => {
this.props.COMMENT_REPLIES.subscribeToMore({
variables: {
eventId: this.props.eventId,
},
document: gql`
subscription newPostReplys($eventId: ID!) {
PostReply(
filter: {
mutation_in: [CREATED]
node: { replyTo: { event: { id: $eventId } } }
}
) {
node {
id
replyTo {
id
}
body
createdAt
author {
id
}
}
}
}
`,
updateQuery: (previous, { subscriptionData }) => {
const {
author,
body,
id,
__typename,
createdAt,
replyTo,
} = subscriptionData.data.PostReply.node;
let newPostReplies = _cloneDeep(previous);
// Test to see if item is already in the store
const idAlreadyExists =
newPostReplies.allPostReplies.filter(item => {
return item.id === id;
}).length > 0;
// Only add it if it isn't already there
if (!idAlreadyExists) {
newPostReplies.allPostReplies.unshift({
author,
body,
id,
__typename,
createdAt,
replyTo,
});
return newPostReplies;
}
},
});
};