apollo-link-state icon indicating copy to clipboard operation
apollo-link-state copied to clipboard

defaultOptions with fetchPolicy: 'cache-and-network' causes local states to be overwritten with defaults

Open isopterix opened this issue 6 years ago • 21 comments

Hi,

I ran into an issue when setting the global fetchPolicy setting for query and watchQuery to cache-and-network while using apollo-link-state and apollo-cache-persist.

My defaultOptions in my Apollo configuration look like this:

const defaultOptions = {
  watchQuery: {
    fetchPolicy: 'cache-and-network',
    errorPolicy: 'all',
  },
  query: {
    fetchPolicy: 'cache-and-network',
    errorPolicy: 'all',
  },
  mutate: {
    errorPolicy: 'all',
  }
}

For whatever reason I cannot get Apollo to grab the persistent cache from the browser and use the client state flag to assess whether a user is logged in or not.

When I set the queries' fetchPolicy manually to cache-and-network without defining it globally via the defaultOptions tag in the Apollo configuration everything works fine. However, if I use the same approach to fetch the local state via the "@client" directive, the local state read from the cache is somehow overwritte with the default when the network re-fetch is initiated shortly after the cache is initially read. Hence, the user always sees the login screen as the appUserIsLoggedin setting is always reset to false.

Below are my two config files. Note that in this version the defaultOptions are commented out and hence the Q_GET_APP_LOGIN_STATE query in the index file is fetched standard cache-first method. THIS WORKS AS EXPECTED!

However, if I activate the defaultOptions setting OR manually set fetchPolicy for the Q_GET_APP_LOGIN_STATE query to cache-and-network, the appUserIsLoggedin variable is initially true when loading the page and a cache with it set to true is present. However, shortly after, the local-state variable is automatically set to false again for whatever reason. I assume this is the result of the automated network re-fetch.

PLEASE NOTE: This ONLY affects data which is stored in the local-state. Data fetched from the network works as expected regardless of the used fetchPolicy setting.

Any ideas what may be the cause for this?

My React Index file:

import React, {Component, Fragment} from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from "react-router-dom"
import { Route, Redirect, Switch } from "react-router-dom"

// LOCALE SUPPORT
import { IntlProvider, addLocaleData } from 'react-intl'
import en from 'react-intl/locale-data/en'

// APOLLO CLIENT
import { ApolloProvider, Query } from 'react-apollo'
import gql from "graphql-tag"
import ApolloClientConfig from "./components/shared/ApolloClientConfig"

// LOAD APP
import App from './App'
import UserLoginForm from "./components/pages/UserLoginForm"
import AppNotifications from "./components/AppNotifications"
import registerServiceWorker from './registerServiceWorker'

// HELPERS
import _ from "lodash"

// CSS
import "semantic-ui-css/semantic.min.css"
import "./index.css"

// ACTIVATE LOCALE SUPPORT
addLocaleData([...en])



// DEFINE PROTECTED ROUTE
const PrivateRoute = ({ component: Component, userLoginStatus, ...rest }) => (
  <Route {...rest} render={(props) => (
    userLoginStatus ? (
      <Component {...props}/>
    ) : (
      <Redirect push to={{
        pathname: '/login',
        state: { from: props.location }
      }}/>
    )
  )}/>
)

// QUERY FOR USER STATE
const Q_GET_APP_LOGIN_STATE = gql`
  query getUserDataFromCache {
    appUserIsLoggedin @client
    appUser @client
  }
`

// DEFINE INITIAL ROUTE
class Init extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
  render() {
    return (
      <Query query={Q_GET_APP_LOGIN_STATE}>
        {({ data }) => (
          <Fragment>
            <AppNotifications/>
            <Switch>
              <Route exact path="/login" component={UserLoginForm} />
              <PrivateRoute path="/" userLoginStatus={data.appUserIsLoggedin} component={App} />
              <Redirect push to="/" />
            </Switch>
          </Fragment>
        )}
      </Query>
    )
  }
}


// INITIATE REACT MAIN APP AND LOGIN REDIRECT
const rootDOM = document.getElementById("root")
ReactDOM.render(
      <ApolloClientConfig
        render={({ restored, client }) =>
          restored ? (
            <BrowserRouter>
              <ApolloProvider client={client}>
                <Init/>
              </ApolloProvider>
            </BrowserRouter>
          ) : (
            <div>Loading cache if available...</div>
          )
        }
      />,
  rootDOM
)

// REGISTER SERVICE WORKER
registerServiceWorker()

My Apollo configuration:

import React, { Component } from 'react'

import { ApolloClient } from 'apollo-client'
import { ApolloLink } from 'apollo-link'
import { setContext } from 'apollo-link-context'
import { InMemoryCache, defaultDataIdFromObject } from 'apollo-cache-inmemory'
import { persistCache, CachePersistor } from 'apollo-cache-persist'
import { HttpLink } from 'apollo-link-http'
import { onError } from 'apollo-link-error'
import { withClientState } from 'apollo-link-state'
import { ApolloProvider, Query } from 'react-apollo'

import _ from 'lodash'

import AppLocalState from "../../resolvers/AppLocalState"

// LOAD QUERIES
import { M_CHANGE_APP_SETTING } from "../../graphql/queries"
import { M_PUSH_APP_NOTIFICATION } from "../AppNotifications"


/////////////////////////////////////////////////////////////////////////////////////
// APOLLO CONFIG
/////////////////////////////////////////////////////////////////////////////////////

const SCHEMA_VERSION = '1'
const SCHEMA_VERSION_KEY = 'LionToDoApp-Schema-Version'

const queryInProcessNotifier = new ApolloLink((operation, forward) => {
  client.mutate({mutation:M_CHANGE_APP_SETTING, variables: { setting:"appIsLoading", state:true }})
  return forward(operation).map((data) => {
    client.mutate({mutation:M_CHANGE_APP_SETTING, variables: { setting:"appIsLoading", state:false }})
    return data
  })
})

const cache = new InMemoryCache({
  dataIdFromObject: object => {
    switch (object.__typename) {
      // other cases here
      default: 
        return defaultDataIdFromObject(object)
    }
  }
})

const persistor = new CachePersistor({
  cache,
  storage: window.localStorage,
  key: "LionToDoApp",
})

const httpLink = new HttpLink({
  uri: 'http://localhost:8000/graphql/',
})

const authLink = setContext((_, { headers }) => {
  const token = window.localStorage.getItem('LionToDoApp_jwt_token');
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : ''
    }
  }
})

const stateLink = withClientState({
  ..._.merge(AppLocalState),
  cache
})

const httpLinkWithAuth = authLink.concat(httpLink)

const link = ApolloLink.from([
  onError(({ graphQLErrors, networkError }) => {
    if (graphQLErrors) {
      graphQLErrors.map(({ message, locations, path }) =>
        client.mutate({mutation:M_PUSH_APP_NOTIFICATION, variables: {
          text:`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
          type:"GRAPHQL_ERROR"
        }})
      )
    }
    if (networkError) {
      client.mutate({mutation:M_PUSH_APP_NOTIFICATION, variables: {
        text:`[Network error]: ${networkError}`,
        type:"NETWORK_ERROR"
      }})
    }
  }),
  stateLink,
  queryInProcessNotifier,
  httpLinkWithAuth
])

const defaultOptions = {
  watchQuery: {
    fetchPolicy: 'cache-and-network',
    errorPolicy: 'all',
  },
  query: {
    fetchPolicy: 'cache-and-network',
    errorPolicy: 'all',
  },
  mutate: {
    errorPolicy: 'all',
  }
}

const client = new ApolloClient({
  link,
  cache,
  //defaultOptions,
})

/////////////////////////////////////////////////////////////////////////////////////
// APOLLO COMPONENT SETUP
/////////////////////////////////////////////////////////////////////////////////////

class ApolloClientConfig extends Component {
  state = {
    client: client,
    restored: false
  }

  async componentWillMount() {
    const currentVersion = await localStorage.getItem(SCHEMA_VERSION_KEY);
    if (currentVersion === SCHEMA_VERSION) {
      // If the current version matches the latest version,
      // we're good to go and can restore the cache.
      await persistor.restore()
    } else {
      // Otherwise, we'll want to purge the outdated persisted cache
      // and mark ourselves as having updated to the latest version.
      await persistor.purge()
      await localStorage.setItem(SCHEMA_VERSION_KEY, SCHEMA_VERSION);
    }
    this.setState({ restored: true })
  }

  render() {
    return this.props.render(this.state)
  }
}

export default ApolloClientConfig

My package versions:

"apollo-cache-inmemory": "^1.1.12",
"apollo-cache-persist": "^0.1.1",
"apollo-client": "^2.2.8",
"apollo-link": "^1.2.1",
"apollo-link-context": "^1.0.7",
"apollo-link-error": "^1.0.7",
"apollo-link-http": "^1.5.3",
"apollo-link-state": "^0.4.1",

"react": "^16.3.0",
"react-apollo": "^2.1.2",
"react-dom": "^16.3.0",
"react-intl": "^2.4.0",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.3",

isopterix avatar Apr 06 '18 10:04 isopterix

Btw. the way I solved it for now is to manually set any query with a "@client" directive in it to use

fetchPolicy='cache-first'

I haven't checked this with any mixed client/network queries yet though...

isopterix avatar Apr 06 '18 13:04 isopterix

Even I am facing similar issue where default data is being returned instead of actual updated data from cache. Any updates on this?

raeesaa avatar Apr 10 '18 08:04 raeesaa

Hi @isopterix, can you please provide a stripped down reproduction in CodeSandbox so I can look into this? Thanks!

peggyrayzis avatar Apr 11 '18 22:04 peggyrayzis

Hi @peggyrayzis, will try to put something together shortly.

isopterix avatar Apr 16 '18 12:04 isopterix

I have the same problem even with setting fetchPolicy='cache-first'

benseitz avatar Apr 19 '18 16:04 benseitz

I tried to put something together... but for whatever reason I am getting "Cannot read property 'Query' of undefined" in the console... https://codesandbox.io/s/82m9r8p379

isopterix avatar Apr 27 '18 15:04 isopterix

Any updates on this? I had to remove defaults as work-around for this issue.

raeesaa avatar May 18 '18 05:05 raeesaa

Also experiencing this issue. With a mixed client/network query, the issue seems to still occur even when using fetchPolicy='cache-first'.

aaronp-hd avatar Jun 12 '18 06:06 aaronp-hd

I'm seeing the same issue. Defaults overwrite the persisted cache when a global config of cache-and-network is set.

craigmulligan avatar Jun 28 '18 10:06 craigmulligan

Hi, Is there a temporary fix or work-around for this?

ramakrishnamundru avatar Jul 19 '18 11:07 ramakrishnamundru

I’m experiencing a similar issue. I’m using next.js and cache is being filled on a server side, dumped to var, which is passed to the browser and used for rehydration during cache initialization on the browser side. Problem occurs when I’m trying to query cache after - it contains only defaults from apollo-link-state. It looks like setting defaults doesn’t care if there is something in the cache already.

pawelsamsel avatar Jul 20 '18 16:07 pawelsamsel

Same for me :(

bslipek avatar Aug 27 '18 20:08 bslipek

Related: https://github.com/apollographql/apollo-link-state/issues/262

mbrowne avatar Aug 27 '18 21:08 mbrowne

I've got same issue, and fixed it by removing the defaults from withClientState(), and writing them directly in the apollo cache.

Here is my code:

const DEFAULT_STATE = {
  networkStatus: {
    __typename: 'NetworkStatus',
    isConnected: true,
    isWebSocketSupported: true,
  },
};

  const stateLink = withClientState({
    cache: apolloCache,
    resolvers: {
      Query: {},
      Mutation: {},
    },
    //    defaults: DEFAULT_STATE,
  });

  apolloCache.writeData({ data: DEFAULT_STATE });

svengau avatar Jan 04 '19 22:01 svengau

Same problem here. I've broken it down to 2 separate issues:

  1. The defaults are written to cache AFTER rehydration, thereby overwriting what was persisted.
  2. I was having some similar problems with cache-first where the onCompleted callback would never be called. cache-and-network seemed to solve the problem, but really its a race condition and cache-and-network just switches up the race a bit. The problem is that the query is trying to run before/during store rehydration. Like most people the first thing my app does is run a query using the Query component which happens on first render. Any kind of delay will hide this problem. At first I tried putting in a timer, but then I realized it was enough to wait until the component mounted. Still, depending on the environment, its probably still vulnerable to breaking under the right conditions. Either apollo needs to be smart enough to queue queries until the store is rehydrated, or we need access to the rehydration state.

jpaas avatar Jan 30 '19 14:01 jpaas

Oh BTW this guy seems to have found a workaround for not overwriting with defaults: https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/195#issuecomment-428668407

jpaas avatar Jan 30 '19 14:01 jpaas

apollo-link-state is in the process of being integrated into apollo-client, including multiple bug fixes and new features. For more info, see https://github.com/apollographql/apollo-client/pull/4338. You can try it out by installing apollo-client@alpha.

I'm not personally involved in the development and haven't tested to see if it fixes this bug, but my understanding is that it should be fixed by the time apollo-client 2.5 is released. Note that the API is still subject to change.

mbrowne avatar Jan 30 '19 20:01 mbrowne

Anyone who can explain why cache-and-network is not working with query?

maziarz avatar Feb 18 '19 09:02 maziarz

Guys, I'm having very similar issues. Have a good look at your indexing logic. Any null keys are not cached. Worse: non-unique keys tend to be overwritten. Make sure this only happens when you want it to.

fozzarelo avatar Mar 08 '19 19:03 fozzarelo

Apollo 2.5 has been released. I doubt there will be any further changes to this library now that local state has been integrated into the core.

mbrowne avatar Mar 09 '19 12:03 mbrowne

@peggyrayzis could we move this to the apollo repository?

adrienharnay avatar Apr 04 '19 13:04 adrienharnay