Использование подписки и обновления после мутации создает дублирующийся узел - с Apollo Client

Я использую update после мутации, чтобы обновить магазин при создании нового комментария. Еще у меня есть подписка на комментарии на этой странице.

Любой из этих методов сам по себе работает должным образом. Однако, когда у меня есть оба, пользователь, создавший комментарий, дважды увидит комментарий на странице и получит эту ошибку от 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.handleChangeEvent = this.handleChangeEvent.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
        this.onKeyDown = this.onKeyDown.bind(this);
    }

    handleChangeEvent(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.handleChangeEvent}
                    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)

Не могли бы вы опубликовать какой-нибудь соответствующий код? Возможно, операция подписки и обновление мутации. Кроме того, будет полезно полное сообщение об ошибке.

Tal Z 18.03.2018 12:57
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
9
1
1 972
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я наткнулся на ту же проблему и не нашел простого и понятного решения.

Что я сделал, так это использовал функцию фильтрации резолвера подписки на сервере. Вы можете следовать этому руководство, который описывает, как настроить сервер и этот руководство для клиента.

Коротко:

  • Добавьте какой-нибудь идентификатор сеанса браузера. Может быть, это токен 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"
});

...
  • Добавить тип подписки с определенным идентификатором в качестве параметра на сервере

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);
}
Ответ принят как подходящий

На самом деле это довольно легко исправить. Я долго был смущен, так как мои подписки периодически прекращались. Оказалось, что это была проблема Graphcool, переключение с кластера из Азии на кластер США остановило нестабильность.

Вам просто нужно проверить, существует ли уже идентификатор в магазине, и не добавлять его, если он есть. Я добавил комментарии к коду, где я изменил код:

_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;
                }
            },
        });
    };

Другие вопросы по теме