4
votes

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

1
I'm kind of confused by your question. It's not that apollo doesn't have unique / multiple loading states, its your code that only has one mutationLoading. state that you pass to your todos. the same state gets passed to all of them, so even if apollo had something you want, your code still wouldn't be making use of it. it would have to be something like loadingId === id at the very least. Anyways your problem is completely solved by calling useMutation at the item level, not the list level. that's not a workaround, it's perfectly sane solutionazium
on another note, apollo does keep track of which query is running. so you could combine your isLoading with the knowledge of which variable was being passed to the mutation, though I would consider that to be a much worse solutionazium
I know that the current code wouldn't take advantage of any per-item state returned from useMutation. I know you'd wanna do something like loadingId === id, but loadingId isn't available.Weston
As for "you could combine your isLoading with the knowledge of which variable was being passed to the mutation", how would that work if multiple mutations were in progress at the same time? loading would mean nothing if I don't know exactly which mutations are the ones actually loading.Weston
And as for pushing the mutation down to the item level, I don't disagree that that's a sane solution, but it does have drawbacks. Namely, the parent component loses any insight into the loading state of its items. Say I wanted to surface loading state for the todo items in the Todos component (for some reason) I would either lose that ability, or need to add some sort of onLoading callback to the Todo components, and still manually keep track of loading state in Todos.Weston

1 Answers

7
votes

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] />
))