我在创建新评论后使用更新后更新来更新商店。我也订阅了此页面上的评论。
这些方法中的任何一个都可以按预期工作。但是当我同时拥有它们时,创建注释的用户将在页面上看到两次注释并从React中获取此错误:
Warning: Encountered two children with the same key,
我认为原因是突变更新和订阅都返回一个新节点,创建一个重复的条目。有没有推荐的解决方案?我在Apollo文档中看不到任何内容,但它对我来说似乎不是一个边缘用例。
这是我订阅的组件:
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;
这是NewComment组件:
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;
完整的错误消息是:
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)
答案 0 :(得分:4)
这实际上非常容易修复。我很困惑,因为我的订阅会间歇性地失败。事实证明,这是一个Graphcool问题,从亚洲到美国集群的转变阻止了这种脆弱。
您只需要测试以查看商店中是否已存在该ID,如果存在,则不添加该ID。我添加了代码注释,我已经更改了代码:
_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;
}
},
});
};
答案 1 :(得分:1)
我偶然发现了同样的问题,并没有找到一个简单而干净的解决方案。
我所做的是在服务器上使用订阅解析器的过滤功能。您可以按照此tutorial来说明如何为客户端设置服务器和tutorial。
简而言之:
type Query {
getBrowserSessionId: ID!
}
Query: {
getBrowserSessionId() {
return 1; // some uuid
},
}
...
if (!getBrowserSessionIdQuery.loading) {
localStorage.setItem("browserSessionId", getBrowserSessionIdQuery.getBrowserSessionId);
}
...
const getBrowserSessionIdQueryDefinition = gql`
query getBrowserSessionId {
getBrowserSessionId
}
`;
const getBrowserSessionIdQuery = graphql(getBrowserSessionIdQueryDefinition, {
name: "getBrowserSessionIdQuery"
});
...
type Subscription {
messageAdded(browserSessionId: ID!): Message
}
import { withFilter } from ‘graphql-subscriptions’;
...
Subscription: {
messageAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(‘messageAdded’),
(payload, variables) => {
// do not update the browser with the same sessionId with which the mutation is performed
return payload.browserSessionId !== variables.browserSessionId;
}
)
}
}
...
const messageSubscription= gql`
subscription messageAdded($browserSessionId: ID!) {
messageAdded(browserSessionId: $browserSessionId) {
// data from message
}
}
`
...
componentWillMount() {
this.props.data.subscribeToMore({
document: messagesSubscription,
variables: {
browserSessionId: localStorage.getItem("browserSessionId"),
},
updateQuery: (prev, {subscriptionData}) => {
// update the query
}
});
}
`Mutation {
createMessage(message: MessageInput!, browserSessionId: ID!): Message!
}`
...
createMessage: (_, { message, browserSessionId }) => {
const newMessage ...
...
pubsub.publish(‘messageAdded’, {
messageAdded: newMessage,
browserSessionId
});
return newMessage;
}
const createMessageMutation = gql`
mutation createMessage($message: MessageInput!, $browserSessionId: ID!) {
createMessage(message: $message, browserSessionId: $browserSessionId) {
...
}
}
`
...
graphql(createMessageMutation, {
props: ({ mutate }) => ({
createMessage: (message, browserSessionId) => {
return mutate({
variables: {
message,
browserSessionId,
},
update: ...,
});
},
}),
});
...
_onSubmit = (message) => {
const browserSessionId = localStorage.getItem("browserSessionId");
this.props.createMessage(message, browserSessionId);
}