Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Managing multiple calls to the same Apollo mutation

So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states

function Todos() {
...
  const [
    updateTodo,
    { loading: mutationLoading, error: mutationError },
  ] = useMutation(UPDATE_TODO);
...

  return data.todos.map(({ id, type }) => {
    let input;

    return (
      <div key={id}>
        <p>{type}</p>
        <form
          onSubmit={e => {
            e.preventDefault();
            updateTodo({ variables: { id, type: input.value } });

            input.value = '';
          }}
        >
          <input
            ref={node => {
              input = node;
            }}
          />
          <button type="submit">Update Todo</button>
        </form>
        {mutationLoading && <p>Loading...</p>}
        {mutationError && <p>Error :( Please try again</p>}
      </div>
    );
  });
}

This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.

enter image description here

And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".

Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.

And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?

Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?

Code Sandbox: https://codesandbox.io/s/v3mn68xxvy

like image 846
Weston Avatar asked Jan 26 '23 14:01

Weston


1 Answers

Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.

The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.

In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:

const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)

If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.

const ToDo = ({ id, type }) => {
  const [value, setValue] = useState('')
  const options = { variables = { id, type: value } }
  const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
  const handleChange = event => setValue(event.target.value)

  return (
    <div>
      <p>{type}</p>
      <form onSubmit={updateTodo}>
        <input
          value={value}
          onChange={handleChange}
        />
        <button type="submit">Update Todo</button>
      </form>
    </div>
  )
}

// back in our original component...

return data.todos.map(({ id, type }) => (
  <Todo key={id} id={id} type={type] />
))

like image 183
Daniel Rearden Avatar answered Jan 28 '23 09:01

Daniel Rearden