You are glossing over the fact that there is a 3rd part, writing a reducer! This part is the most cumbersome in my opinion.
Lets say you wanted to keep a counter and increment it in your app. With Redux, you add it to your state, write an "increaseCounter" action, and then write a reducer. This reducer is just a plain function without any dependencies. A unit test for this reducer would just test that the count was updated.
However, with local state management with Apollo, you would either need to
1. write an incrementCounter mutation and an incrementCounter reducer. Inside that reducer, you would have to fetch the current count from the cache, update that count, and then write the results from the cache.
2. Just use `cache.writeQuery` or `cache.writeData`, but in either case, you need to fetch the current state of the cache, modify it, and then write the object back.
The problem with this is that any time you want to modify state, you need to always fetch the current state, and then write the state back. This is bad because
1. its super repetitive
2. it tightly binds the code of "how the state changes" to "how the state is queried and saved"
Overall, this whole approach of having to write a mutation and a reducer for every state change is annoying and verbose. Redux is just overall a better, more convenient pattern to incrementally update a piece of state.
We actually thought about adding a Redux layer on top of Apollo, so that we would just have a single "Redux" mutation that fetches the current state, passes it to a reducer, and then saves the transformed state in the cache to avoid all this fetching and saving, but then were afraid of potential performance issues of homerolling our own Redux, and went with actual Redux instead.