apollographql / react-apollo

:recycle: React integration for Apollo Client
https://www.apollographql.com/docs/react/
MIT License
6.85k stars 787 forks source link

Why doesn't writeFragment update queries that use the fragment? #4001

Open roballsopp opened 4 years ago

roballsopp commented 4 years ago

Here's a mutation config I have that uses writeFragment in the updater:

export const createSurveyResponseMutation = gql`
    mutation createSurveyResponse($input: CreateSurveyResponseInput!) {
        createSurveyResponse(input: $input) {
            id
            surveyTemplateId
            ...SurveyQueryContainer_surveyResponse
        }
    }
    ${SurveyQueryContainerGql.surveyResponseFragment}
`;

export const getCreateSurveyResponseMutationConfig = ({ variables }) => ({
        mutation: createSurveyResponseMutation,
        variables,
        optimisticResponse: {
            createSurveyResponse: {
                ...variables.input,
                __typename: 'SurveyResponse',
                questionResponses: { __typename: 'ModelQuestionResponseConnection', items: [] },
            },
        },
        update: (store, { data: { createSurveyResponse } }) => {
            store.writeFragment({
                id: `SurveyResponse:${createSurveyResponse.id}`,
                fragment: SurveyQueryContainerGql.surveyResponseFragment,
                fragmentName: 'SurveyQueryContainer_surveyResponse',
                data: createSurveyResponse,
            });
        },
    });

Here's the component using the query I want to update:

import React from 'react';
import PropTypes from 'prop-types';
import { Query } from '@apollo/react-components';
import SurveyNavigator from './SurveyNavigator';
import { SurveyQueryContainerQuery } from './graphql/SurveyQueryContainer';

export default function SurveyQueryContainer({ route, navigation }) {
    const { surveyTemplateId, surveyResponseId } = route.params;

    return (
        <Query
            variables={{ surveyTemplateId, surveyResponseId }}
            query={SurveyQueryContainerQuery}>
            {({ loading, error, data, refetch }) => {
                return <SurveyNavigator surveyTemplate={data.getSurveyTemplate} surveyResponse={data.getSurveyResponse} />;
            }}
        </Query>
    );
}

And here's the query:

import gql from 'graphql-tag';
import { SurveyNavigatorFragments } from '../SurveyNavigator';

export const surveyTemplateFragment = gql`
    fragment SurveyQueryContainer_surveyTemplate on SurveyTemplate {
        id
        name
        ...SurveyNavigator_surveyTemplate
    }
    ${SurveyNavigatorFragments.surveyTemplate}
`;

export const surveyResponseFragment = gql`
    fragment SurveyQueryContainer_surveyResponse on SurveyResponse {
        id
        ...SurveyNavigator_surveyResponse
    }
    ${SurveyNavigatorFragments.surveyResponse}
`;

export const SurveyQueryContainerQuery = gql`
    query SurveyQueryContainerQuery($surveyTemplateId: ID!, $surveyResponseId: ID!) {
        getSurveyTemplate(id: $surveyTemplateId) {
            ...SurveyQueryContainer_surveyTemplate
        }
        getSurveyResponse(id: $surveyResponseId) {
            ...SurveyQueryContainer_surveyResponse
        }
    }
    ${surveyTemplateFragment}
    ${surveyResponseFragment}
`;

I know the getSurveyTemplate field is in the cache since I've visited this screen before and the data for that field is always the same (the surveyTemplateId variable is always the same). The only thing in the SurveyQueryContainerQuery that changes each time is the surveyResponseId variable and therefore the SurveyResponse. Given this setup, if I fire my createSurveyResponse mutation before visiting the screen with SurveyQueryContainer in it, I would expect the query component to render immediately because I have optimistically updated the cache with the correct SurveyQueryContainer_surveyResponse fragment. I should have all the data needed to satisfy SurveyQueryContainerQuery present in the cache, so it shouldn't need to do a network fetch, yet it is still doing this. Am I doing something wrong here? Why can't it find the fragment I've written?

This seems to work when I change my updater to:

...
update: (store, { data: { createSurveyResponse } }) => {
    let getSurveyTemplate;
    try {
        getSurveyTemplate = store.readFragment({
            id: `SurveyTemplate:${createSurveyResponse.surveyTemplateId}`,
            fragment: SurveyQueryContainerGql.surveyTemplateFragment,
            fragmentName: 'SurveyQueryContainer_surveyTemplate',
        });
    } catch (e) {
        console.warn("Could not read survey template from store", e);
    }

    store.writeQuery({
        query: SurveyQueryContainerGql.SurveyQueryContainerQuery,
        variables: {
            surveyTemplateId: createSurveyResponse.surveyTemplateId,
            surveyResponseId: createSurveyResponse.id,
        },
        data: {
            getSurveyTemplate,
            getSurveyResponse: createSurveyResponse,
        },
    });
},
...

But this is quite a bit more verbose, and seems like it shouldn't be necessary.

roballsopp commented 4 years ago

My writeFragment example works as expected with a cache redirect:

new ApolloClient({
    ...
    cache: new InMemoryCache({
        cacheRedirects: {
            Query: {
                getSurveyResponse: (_, args, { getCacheKey }) => getCacheKey({ __typename: 'SurveyResponse', id: args.id }),
            },
        },
    }),
    ...
})

Still very curious why this doesn't seem to be the default behavior, and what I'm breaking by doing this.