Help with refetchQueries onDeleteClick

I am working with refetchQueries after a delete and running into issues. The item deletes no problem, but it continues to be displayed until the page refreshes. It has worked intermittently. I am new to GraphQL so I must be missing how this works.

I don’t think I should need all those refetchQueries. Thank you in advance for any insight.

here is the component where the delete is happening:

import { useContext } from 'react'

import {
  Form,
  FormError,
  FieldError,
  Label,
  TextField,
  Submit,
} from '@redwoodjs/forms'
import { Link, routes, navigate } from '@redwoodjs/router'
import { MetaTags, useMutation } from '@redwoodjs/web'
import { Toaster, toast } from '@redwoodjs/web/toast'

import { QUERY } from 'src/components/ClassListCell/ClassListCell'
import { StudentContext } from 'src/context/StudentContext'

const DELETE_STUDENT_MUTATION = gql`
  mutation DeleteStudentMutation($id: Int!) {
    deleteStudent(id: $id) {
      id
    }
  }
`
const StudentForm = (props, { loading, error, formMethods }) => {
  const { isEdit, isDelete, hasChanged, setHasChanged, setIsDelete } =
    useContext(StudentContext)

  const onSubmit = (data) => {
    props.onSave(data, props?.student?.id, props?.student?.order)
  }

  const [deleteStudent] = useMutation(DELETE_STUDENT_MUTATION, {
    onCompleted: () => {
      toast.success('Student deleted')
      setIsDelete(false)
      navigate(routes.classList())
    },
    onError: (error) => {
      toast.error(error.message)
    },
    refetchQueries: [
      { query: QUERY, variables: { userId: props?.student?.userId } },
    ],
    awaitRefetchQueries: true,
  })

  const onDeleteClick = (id) => {
    deleteStudent({
      variables: { id },
      update: (cache) => {
        let data = JSON.parse(JSON.stringify(cache.readQuery({ query: QUERY })))
        const updatedStudents = data.students.filter(
          ({ id: studentId }) => studentId !== id
        )
        cache.writeQuery({
          query: QUERY,
          variables: { userId: props?.student?.userId },
          data: { students: updatedStudents },
        })
      },
      refetchQueries: [
        { query: QUERY, variables: { userId: props?.student?.userId } },
      ],
      awaitRefetchQueries: true,
    })
    setIsDelete(false)
  }

  return (
    <>
      <MetaTags title="Student Form " description="Student page" />
      {!isDelete && (
        <>
          <h3 className="my-14 font-bold text-center text-xl">
            {isEdit
              ? `Please update the details for ${props?.student?.displayName}.`
              : 'Add a student to your list.'}
          </h3>
          <div className="rw-segment-main w-2/5 m-auto">
            <Form
              onSubmit={onSubmit}
              formMethods={formMethods}
              config={{ mode: 'onBlur' }}
              error={error}
            >
              <FormError error={error} wrapperClassName="form-error" />
              <Label htmlFor="firstName">First Name</Label>
              <TextField
                defaultValue={props?.student?.firstName}
                name="firstName"
                validation={{ required: true }}
                errorClassName="error"
                onChange={() => setHasChanged(true)}
                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              />
              <FieldError name="firstName" className="error" />

              <Label htmlFor="lastName">Last Name</Label>
              <TextField
                name="lastName"
                defaultValue={props?.student?.lastName}
                validation={{ required: true }}
                errorClassName="error"
                onChange={() => setHasChanged(true)}
                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              />
              <FieldError name="lastName" className="error" />

              <Label htmlFor="birthday">Birthday</Label>
              <TextField
                name="birthday"
                defaultValue={props?.student?.birthday}
                validation={{ required: true }}
                errorClassName="error"
                onChange={() => setHasChanged(true)}
                className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              />
              <FieldError name="birthday" className="error" />
              <div className="rw-button-group">
                <Submit
                  className="hover:text-pink-800 border-pink-500 border-2 rounded-lg p-2 mt-4"
                  disabled={loading || !hasChanged}
                >
                  {isEdit ? 'Save' : 'Create'}
                </Submit>
                <button
                  disabled={loading}
                  className="hover:text-pink-800 border-pink-500 border-2 rounded-lg p-2 mt-4 ml-5"
                >
                  <Link to={routes.classList()}>Cancel</Link>
                </button>
              </div>
            </Form>
          </div>
        </>
      )}
      {isDelete && (
        <div className="antialiased bg-gray-200 text-gray-900 font-sans overflow-x-hidden z-50 mt-0">
          <div className="relative px-4 min-h-screen md:flex md:items-center md:justify-center">
            <div className="bg-black opacity-25 w-full h-full absolute z-10 inset-0"></div>
            <div className="bg-white rounded-lg md:max-w-md md:mx-auto p-4 fixed inset-x-0 bottom-0 z-50 mb-4 mx-4 md:relative">
              <div className="md:flex items-center">
                <div className="rw-segment">
                  <header className="rw-segment-header">
                    <h2 className="text-2xl text-center font-bold ">
                      <div className="rounded-full border border-gray-300 flex items-center justify-center w-16 h-16 flex-shrink-0 mx-auto">
                        <i className="lni lni-warning"></i>
                      </div>
                      Are you sure you want to delete{' '}
                      {props?.student?.firstName} {props?.student?.lastName}?
                    </h2>
                  </header>
                  <div className="rw-segment-main">
                    <p className="text-sm text-center text-gray-700 mt-1">
                      This action cannot be undone.
                    </p>
                  </div>
                  <div className="rw-button-group">
                    <button
                      className="hover:text-red-700 hover:bg-red-200 border-pink-500 border-2 rounded-lg p-2 mt-4"
                      onClick={() => onDeleteClick(props?.student?.id)}
                    >
                      <Link to={routes.classList()}>Delete</Link>
                    </button>
                    <button className="hover:text-pink-800 border-pink-500 border-2 rounded-lg p-2 mt-4 ml-5">
                      <Link to={routes.classList()}>Cancel</Link>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      )}
      <Toaster />
    </>
  )
}

export default StudentForm

Hey Lisa,

I found this blog post by Apollo, which looks like what you are doing with update, and they do not call refetchQueries at all. The update function apparently triggers the UI refresh. But, I am not experienced with apollo cache, so I can’t confirm. I asked the core-team for thoughts, and a working example repo was requested to help look into it.

Would you mind providing a runnable example that reproduces the problem?

Regards,

Barrett

It’s been a while since I messed with the cache, but I remember being in a situation where deleting a record via a mutation did not remove the record from the local Apollo cache in the browser, so it kept showing, just like what you’re experiencing. I had to manually remove the record via the update callback on useMutation(). You can see an example here: apollo - ApolloClient: Delete item from cache across all queries? - Stack Overflow

That looks like the Apollo v2 way, in v3 they apparently introduced an evict() function which is much easier to deal with (I assume you would still put this in the update() callback where you have access to the cache object): Garbage collection and cache eviction - Apollo GraphQL Docs

Another thought, from @danny:

Honestly 90% of the time this is caused by the cache strategy in Apollo. I think sometimes apollo can’t hash objects correctly, for example if you don’t fetch the id as part of the query.

First thing I would do is set the cache strategy (Cells | RedwoodJS Docs) to ‘no-cache’ to see if it goes away.

Then adjust to find one that works

Thank you for these thoughts.

In the end it seems I needed to implement both!

In the cell i included:

export const beforeQuery = (props) => {
  return { variables: props, fetchPolicy: 'no-cache', pollInterval: 2500 }
}

And then with the delete functions I refactored to use both refetchQueries and cache.evict():

  const [deleteStudent] = useMutation(DELETE_STUDENT_MUTATION, {
    onCompleted: () => {
      toast.success('Student deleted')
      setIsDelete(false)
      navigate(routes.classList())
    },

    onError: (error) => {
      toast.error(error.message)
    },
    refetchQueries: [{ query: QUERY }],
    awaitRefetchQueries: true,
    update: (cache) => {
      cache.evict({ id: props?.student?.id })
    },
  })

  const onDeleteClick = (id) => {
    deleteStudent({
      variables: { id },
      update: (cache) => {
        cache.evict({ id: id })
      },
    })
    setIsDelete(false)
  }

This seems a little hacky because I had to put evict() in both, but it works and is good enough for now.

Thank you! I am new to Apollo so your thoughts are much appreciated.