30

I have a mutation like

mutation deleteRecord($id: ID) {
    deleteRecord(id: $id) {
        id
    }
}

and in another location I have a list of elements.

Is there something better I could return from the server, and how should I update the list?

More generally, what is best practice for handling deletes in apollo/graphql?

derekdreery
  • 3,280
  • 4
  • 27
  • 38
  • Note to self: This page may be useful http://dev.apollodata.com/react/cache-updates.html#updateQueries – derekdreery Nov 08 '16 at 10:56
  • 14
    TLDR: Basically, you don't. Instead, you loose your hair, curse the Apollo team in a loop, and go through a huge list of compromising half-working workarounds provided by users like you on their Github page. https://github.com/apollographql/apollo-client/issues/621 – Vincent Cantin Apr 27 '18 at 02:14
  • I can almost guarantee that someday there will be a way to invalidate the deleted item such that Apollo automatically refetches any queries containing it, because the current ways of doing this are very far from perfect. – Andy Jul 17 '18 at 22:18

6 Answers6

19

I am not sure it is good practise style but here is how I handle the deletion of an item in react-apollo with updateQueries:

import { graphql, compose } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'react-addons-update';
import _ from 'underscore';


const SceneCollectionsQuery = gql `
query SceneCollections {
  myScenes: selectedScenes (excludeOwner: false, first: 24) {
    edges {
      node {
        ...SceneCollectionScene
      }
    }
  }
}`;


const DeleteSceneMutation = gql `
mutation DeleteScene($sceneId: String!) {
  deleteScene(sceneId: $sceneId) {
    ok
    scene {
      id
      active
    }
  }
}`;

const SceneModifierWithStateAndData = compose(
  ...,
  graphql(DeleteSceneMutation, {
    props: ({ mutate }) => ({
      deleteScene: (sceneId) => mutate({
        variables: { sceneId },
        updateQueries: {
          SceneCollections: (prev, { mutationResult }) => {
            const myScenesList = prev.myScenes.edges.map((item) => item.node);
            const deleteIndex = _.findIndex(myScenesList, (item) => item.id === sceneId);
            if (deleteIndex < 0) {
              return prev;
            }
            return update(prev, {
              myScenes: {
                edges: {
                  $splice: [[deleteIndex, 1]]
                }
              }
            });
          }
        }
      })
    })
  })
)(SceneModifierWithState);
vwrobel
  • 1,496
  • 12
  • 22
  • Hi @vwrobel thanks for the answer. I'm interested in removing any remenant of this record in the cache (I use `${type}:${id}` for my cache key). Would this do that as well? – derekdreery Nov 22 '16 at 22:02
  • Hi @derekdreery. When I use `updateQuery`, only the specified code is applied to previous query results. In the code I've posted, the only change I get is that my deleted item is removed from list SceneCollections.myScenes. Besides, if I have an other query to get `user { id, sceneCounter }`, I would have to update the sceneCounter manually from updateQueries: even if my DeleteSceneMutation returns an updated `user { id, sceneCounter }`, the other query results are not updated when I use updateQueries. Not sure it answers your question, my apollo knowledge is rather limited... – vwrobel Nov 23 '16 at 13:39
  • No worries - my knowledge is limited too!! – derekdreery Nov 24 '16 at 22:20
  • This is awesome, thanks. What happens when the named queries were called with `id`s? Can Apollo figure out which set of data to modify in `updateQueries`? i.e. What if you had multiple results from `SceneCollections(id: "xyz")` in the store already? – Jazzy Feb 01 '17 at 01:49
15

Here is a similar solution that works without underscore.js. It is tested with react-apollo in version 2.1.1. and creates a component for a delete-button:

import React from "react";
import { Mutation } from "react-apollo";

const GET_TODOS = gql`
{
    allTodos {
        id
        name
    }
}
`;

const DELETE_TODO = gql`
  mutation deleteTodo(
    $id: ID!
  ) {
    deleteTodo(
      id: $id
    ) {
      id
    }
  }
`;

const DeleteTodo = ({id}) => {
  return (
    <Mutation
      mutation={DELETE_TODO}
      update={(cache, { data: { deleteTodo } }) => {
        const { allTodos } = cache.readQuery({ query: GET_TODOS });
        cache.writeQuery({
          query: GET_TODOS,
          data: { allTodos: allTodos.filter(e => e.id !== id)}
        });
      }}
      >
      {(deleteTodo, { data }) => (
        <button
          onClick={e => {
            deleteTodo({
              variables: {
                id
              }
            });
          }}
        >Delete</button>            
      )}
    </Mutation>
  );
};

export default DeleteTodo;
sinned
  • 568
  • 1
  • 5
  • 15
7

All those answers assume query-oriented cache management.

What if I remove user with id 1 and this user is referenced in 20 queries across the entire app? Reading answers above, I'd have to assume I will have to write code to update the cache of all of them. This would be terrible in long-term maintainability of the codebase and would make any refactoring a nightmare.

The best solution in my opinion would be something like apolloClient.removeItem({__typeName: "User", id: "1"}) that would:

  • replace any direct reference to this object in cache to null
  • filter out this item in any [User] list in any query

But it doesn't exist (yet)

It might be great idea, or it could be even worse (eg. it might break pagination)

There is interesting discussion about it: https://github.com/apollographql/apollo-client/issues/899

I would be careful with those manual query updates. It looks appetizing at first, but it won't if your app will grow. At least create a solid abstraction layer at top of it eg:

  • next to every query you define (eg. in the same file) - define function that clens it properly eg

const MY_QUERY = gql``;

// it's local 'cleaner' - relatively easy to maintain as you can require proper cleaner updates during code review when query will change
export function removeUserFromMyQuery(apolloClient, userId) {
  // clean here
}

and then, collect all those updates and call them all in final update

function handleUserDeleted(userId, client) {
  removeUserFromMyQuery(userId, client)
  removeUserFromSearchQuery(userId, client)
  removeIdFrom20MoreQueries(userId, client)
}
Adam Pietrasiak
  • 10,495
  • 6
  • 66
  • 82
  • This is exactly my issue, I have many queries/lists in the cache that use the same value. And I really don't want to keep track of all of them. – Juan Solano Jul 30 '19 at 21:50
5

For Apollo v3 this works for me:

const [deleteExpressHelp] = useDeleteExpressHelpMutation({
  update: (cache, {data}) => {
    cache.evict({
      id: cache.identify({
        __typename: 'express_help',
        id: data?.delete_express_help_by_pk?.id,
      }),
    });
  },
});

From the new docs:

Filtering dangling references out of a cached array field (like the Deity.offspring example above) is so common that Apollo Client performs this filtering automatically for array fields that don't define a read function.

Marcel
  • 126
  • 1
  • 3
4

Personally, I return an int which represents the number of items deleted. Then I use the updateQueries to remove the document(s) from the cache.

Siyfion
  • 969
  • 1
  • 11
  • 31
1

I have faced the same issue choosing the appropriate return type for such mutations when the rest API associated with the mutation could return http 204, 404 or 500.

Defining and arbitrary type and then return null (types are nullable by default) does not seem right because you don't know what happened, meaning if it was successful or not.

Returning a boolean solves that issue, you know if the mutation worked or not, but you lack some information in case it didn't work, like a better error message that you could show on FE, for example, if we got a 404 we can return "Not found".

Returning a custom type feels a bit forced because it is not actually a type of your schema or business logic, it just serves to fix a "communication issue" between rest and Graphql.

I ended up returning a string. I can return the resource ID/UUID or simply "ok" in case of success and return an error message in case of error.

Not sure if this is a good practice or Graphql idiomatic.

Mario
  • 1,083
  • 1
  • 10
  • 35