突变后使用订阅和更新创建重复节点 - 使用 Apollo 客户端
Posted
技术标签:
【中文标题】突变后使用订阅和更新创建重复节点 - 使用 Apollo 客户端【英文标题】:Using subscription and update after mutation creates duplicate node - with Apollo Client 【发布时间】:2018-08-25 01:43:57 【问题描述】:当创建新评论时,我在突变后使用更新来更新商店。我在这个页面上也订阅了 cmets。
这些方法中的任何一种都可以按预期工作。但是,当我同时拥有两者时,创建评论的用户将在页面上看到两次评论并从 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)
【问题讨论】:
你能发布一些相关的代码吗?也许订阅操作和突变更新。此外,完整的错误消息也会有所帮助。 【参考方案1】:我偶然发现了同样的问题,但没有找到简单而干净的解决方案。
我所做的是使用服务器上订阅解析器的过滤功能。你可以关注这个tutorial,它描述了如何设置服务器和这个tutorial为客户端。
简而言之:
添加某种浏览器会话 ID。可能是 JWT 令牌或其他一些唯一键(例如 UUID)作为查询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"
);
...
在服务器上添加具有特定id的订阅类型作为参数
type Subscription
messageAdded(browserSessionId: ID!): Message
在解析器上为浏览器会话 ID 添加过滤器
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;
)
当您向查询添加订阅时,您将浏览器会话 ID 添加为参数
...
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
);
在服务器上的变更中,您还可以添加浏览器会话 ID 作为参数
`Mutation
createMessage(message: MessageInput!, browserSessionId: ID!): Message!
`
...
createMessage: (_, message, browserSessionId ) =>
const newMessage ...
...
pubsub.publish(‘messageAdded’,
messageAdded: newMessage,
browserSessionId
);
return newMessage;
当您调用突变时,您会从本地存储中添加浏览器会话 ID,并在更新功能中执行查询更新。现在,查询应该从发送突变的浏览器上的突变更新,并从订阅中更新其他查询。
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);
【讨论】:
【参考方案2】:这实际上很容易解决。我很困惑,因为我的订阅会间歇性地失败。事实证明这是一个 Graphcool 问题,从亚洲集群切换到美国集群阻止了这种脆弱性。
您只需测试该 ID 是否已存在于商店中,如果存在则不要添加它。我在更改代码的地方添加了代码 cmets:
_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;
,
);
;
【讨论】:
以上是关于突变后使用订阅和更新创建重复节点 - 使用 Apollo 客户端的主要内容,如果未能解决你的问题,请参考以下文章