React-apollo: Direct Write of local state not updating Query

Created on 14 Mar 2019  路  22Comments  路  Source: apollographql/react-apollo

Intended outcome:

Query with local state to update value in render prop data.

Actual outcome:

Query doesn't re-pull in client state and therefore components not re-rendering.

How to reproduce the issue:

...
<Query query={LOGIN_WINDOW_QUERY}>
    {({ data, client }) => {
         ....
    })}
</Query>
...

const LOGIN_WINDOW_QUERY = gql`
  query loginWindowQuery {
    isLoginOpen @client
  }
`;

Running

export const closeLogin = ({ cache }) =>
  cache.writeData({ data: { isLoginOpen: false } });

Isn't causing the query to rerun.

I've double checked and if I do a cache.readData, isLoginOpen is set to true, yet the Query doesn't re-run.

Also Apollo dev tools isn't updating the cache on click although I think that's just a bug with the dev tools themselves.

Version

Most helpful comment

@andrewangelle I don't think that's totally correct.

https://www.apollographql.com/docs/react/essentials/local-state.html#direct-writes

Mutations via a client side resolver were available pre 2.5. A direct write is still a recommended way to update the cache. IMO it's way more lightweight as well.

All 22 comments

@stolinski I'm not sure this is related to react-apollo. I think it's in the way you're attempting to update the cache. Since you're using apollo-client v2.5 the updates should be done through a client-side resolver in the same way you are using client-side queries.

  const client = new ApolloClient({
    ...
    resolvers: {
      Mutation: {
        ...
        closeLogin: (_info, _args, {client}) => {
          client.writeQuery({
            query: LOGIN_WINDOW_QUERY,
            data: { isLoginOpen: false }
          });
          return null
        }
      }
    }
  })``

``javascript export const CLOSE_LOGIN = gql
mutation {
closeLogin @client
}
`;

```javascript
   <Mutation mutation={CLOSE_LOGIN}>
     {closeLogin => <button onClick={() => closeLogin()}>Close</button>}
   </Mutation>

Using this pattern will auto trigger the updates in your UI. I have very similar logic to yours working in my own projects.

@andrewangelle I don't think that's totally correct.

https://www.apollographql.com/docs/react/essentials/local-state.html#direct-writes

Mutations via a client side resolver were available pre 2.5. A direct write is still a recommended way to update the cache. IMO it's way more lightweight as well.

Ran into this as well (#2909). @hwillson what can we do to help this issue along?

@stolinski just out of interest if you try cache.writeQuery instead of cache.writeData does the query re-render?

I have actually had to pull Apollo local state out of my application in favor of React context, so I did not test the writeQuery style. I've had it just one too many issues to stay with Apollo state. That said, the writeData is way less code to do the exact same thing, so if I were still using state in Apollo I probably wouldn't be interested in using writeQuery.

@afenton90 Nope, it doesn't.

@stolinski just out of interest if you try cache.writeQuery instead of cache.writeData does the query re-render?

I'm working on something similar (don't mean to hijack this thread), and doing either writeQuery or writeData does not rerender the component. I might also have to go the redux route. I feel this stuff wasn't really tested outside very base and not real world, usages.

real world, usages

I mean, let's be honest. A <Query /> component not rerendering due to a local state change is a very basic test. I've scoured their <MockedProvider /> test suite and it's tested well, so I'd give the team more credit. I feel this may be a regression.

To everyone here, you can use client.subscribe in the meantime.

Did anyone tried to fallback to [email protected]? I sticked to that version until the (other error) fix arrives and there are no compatibility issues.

@stolinski Sorry for the large delay here. If you change your cache.writeData call to client.writeData, I believe you'll find things work properly. Here's a quick example:

https://codesandbox.io/s/2jr3n707n

This is because client.writeData leads to watched queries being re-broadcast, whereas cache.writeData does not. Sorry for the confusion - we have an issue open in the AC repo (https://github.com/apollographql/apollo-client/issues/4398) to help clarify this further in the docs.

@hwillson this is still failing for me with <MockedProvider />. Should I create a repo that reproduces the problem and open a new GH issue?

Got the problem working here: https://codesandbox.io/s/k35325ojr3; take a look at the test. Hopefully I'm doing something wrong!

@nshoes This is a different issue. MockedProvider creates its own ApolloClient instance to use internally, which means by default it also creates its own InMemoryCache instance. This means the cache manipulations you're making in your CS index.js aren't available when index.test.js runs. There are a few ways you can work around this, but the easiest (for now) is to pass your own cache instance into MockerProvider as a prop (you could export/import/re-use the same cache instance from your index.js, or just create a new one). MockedProvider will then use this with its internal ApolloClient instance, and you can manipulate it in your tests. I've created a new CS that shows this here: https://codesandbox.io/s/r1w2zq66zm

A few more things to note:

  • Yes, this is cumbersome; we're refactoring just about everything in react-apollo for v3.0, including the testing utilities like MockedProvider (which are being moved into their own package). This will get better, I promise!
  • If you just want to test a component that works with local state (ie. the cache / local resolvers), you don't need to pass mocks into MockedProvider. mocks is used to build a link chain, which the local state stuff doesn't need (see the change in the CS).
  • Also, if you want to test AC 2.5's new local state stuff (like local resolvers), you'll have to pass them in as the resolvers prop (you aren't testing them here, but just thought I'd mention this).

@hwillson thanks for the detailed response, I appreciate it. A few questions:

  • In your CS you're writing to the cache before rendering the component, which is the key difference between my tests and yours. I still don't understand why <Query /> doesn't update; will this be fixed in 3.0?
  • "mocks is used to build a link chain, which the local state stuff doesn't need" - Cool, didn't know that! TIL.
  • When does 3.0 get released? :)

@nshoes sorry, writing to the cache like that in the test was just to show that cache changes are taking effect. As long as you're updating the same cache MockedProvider is using behind the scenes, updating should work properly. So the important part is to make sure you specify the cache prop if you want the tests to run with the same cache you're manipulating elsewhere. Here's a slightly different CS that re-uses the cache from the original client in MockedProvider: https://codesandbox.io/s/nwv7jkn24m

Good question on RA 3 - hopefully really soon. I think we're about 3 weeks away from an alpha release (but don't tell anyone I said that ... 馃檪).

@hwillson
I'm trying to update cache in onError hook from 'apollo-link-error'. I found only that way

const httpLink = createHttpLink({
  uri,
});
const errorLink = onError((params) => {      
        const {cache} = params.operation.getContext();
        cache.writeData({data: {tokenValid: false}
})

const apolloClient = new ApolloClient({
  link: errorLink.concat(httpLink),
  cache,
});

But it wouldn't work for subscribed queries as I understand from above.
Is there any way to update all queries that subscribed to tokenValid?

Using client.writeQuery in local state resolvers to update subscribed queries must be mentioned in the docs.
It confused me a bit.

So to update the local state it's better to use client.writeData client.writeQuery cache.writeData cache.writeQuery ? I'm working with apollo for 2 days and I don't know what is better but all do the same thing, I can not find a clear explanation.

@snettah writeQuery. I believe that the one from client is more reliable

I experienced this same issue where client.writeQuery was not propagating to Query observers. Turns out I was mutating the cache on accident. More of a javascript issue but leaving in the hopes it saves someone time.

/**
 * Example of what NOT to do (ie do not mutate the cache!)
 */
const MY_QUERY = gql`
{
  user {
    permissions
  }
}
`
const cachedQuery = client.readQuery({query: MY_QUERY});
const permissions = cachedQuery.user.permissions
// splice mutates the array (which is still a reference to the original cached array!)
permissions.splice(indexOfPermissionToRemove, 1)
client.writeQuery({query: MY_QUERY, data: {
  user: {
    permissions
  } 
}}}

To prevent mutation of the cached array you must create a new array from the existing data. Common technique is to use concat. After this fix apollo was properly propagating changes to the cache.

const permissions = [].concat(cachedQuery.user.permissions)

@nshoes This is a different issue. MockedProvider creates its own ApolloClient instance to use internally, which means by default it also creates its own InMemoryCache instance. This means the cache manipulations you're making in your CS index.js aren't available when index.test.js runs. There are a few ways you can work around this, but the easiest (for now) is to pass your own cache instance into MockerProvider as a prop (you could export/import/re-use the same cache instance from your index.js, or just create a new one). MockedProvider will then use this with its internal ApolloClient instance, and you can manipulate it in your tests. I've created a new CS that shows this here: https://codesandbox.io/s/r1w2zq66zm

@hwillson, this was helpful, my friend. After I implemented this, I was still having some issues, but it was resolved when I realized I needed to set the initial state to the cache once created in the test file (or as you said, just import/export from index.js; should've done that one from the beginning 馃槄). Anyway, in case this is helpful to anyone, adding this (along with the solution in the codesandbox.io link) ended up solving the issue for me:

const cache = new InMemoryCache();

const initialState = {
  testData: [],
};

cache.writeData({
  data: initialState,
});

@nshoes sorry, writing to the cache like that in the test was just to show that cache changes are taking effect. As long as you're updating the same cache MockedProvider is using behind the scenes, updating should work properly. So the important part is to make sure you specify the cache prop if you want the tests to run with the same cache you're manipulating elsewhere. Here's a slightly different CS that re-uses the cache from the original client in MockedProvider: https://codesandbox.io/s/nwv7jkn24m

@hwillson ... I feel like this would solve my problem, but both of your examples seem to be gone, (404). Can you repost? I also need to warm the cache, or my initial queries come back undefined. How to do this in my test?

UPDATE: just saw the post from @vacas above. That will warmed the cache and I got it working now.

Was this page helpful?
0 / 5 - 0 ratings