首页 文章

在突变后使用订阅和更新创建重复节点 - 使用Apollo Client

提问于
浏览
7

我在创建新评论时使用更新后更新来更新商店 . 我也订阅了此页面上的评论 .

这些方法中的任何一种都可以按预期方式工作 . 但是当我同时拥有它们时,创建注释的用户将在页面上看到两次注释并从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)

2 回答

  • 1

    这实际上很容易修复 . 我很困惑,因为我的订阅会间歇性地失败 . 事实证明,这是一个Graphcool问题,从亚洲到美国集群的转变阻止了这种脆弱 .

    您只需要测试以查看商店中是否已存在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;
                    }
                },
            });
        };
    
  • 3

    我偶然发现了同样的问题,并没有找到一个简单而干净的解决方案 .

    我所做的是在服务器上使用订阅解析器的过滤功能 . 您可以按照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);
    }
    

相关问题