Skip to content
This repository has been archived by the owner on Jul 10, 2019. It is now read-only.

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

Open
isopterix opened this issue Apr 6, 2018 · 21 comments

Comments

@isopterix
Copy link

isopterix commented Apr 6, 2018

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
Copy link
Author

isopterix commented Apr 6, 2018

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...

@fbartho fbartho added the bug label Apr 6, 2018
@raeesaa
Copy link

raeesaa commented Apr 10, 2018

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

@peggyrayzis
Copy link
Contributor

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

@isopterix
Copy link
Author

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

@benseitz
Copy link

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

@isopterix
Copy link
Author

isopterix commented Apr 27, 2018

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

@raeesaa
Copy link

raeesaa commented May 18, 2018

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

@aaronp-hd
Copy link

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

@craigmulligan
Copy link

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

@ramakrishnamundru
Copy link

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

@pawelsamsel
Copy link

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.

@bslipek
Copy link

bslipek commented Aug 27, 2018

Same for me :(

@mbrowne
Copy link

mbrowne commented Aug 27, 2018

Related: #262

@svengau
Copy link

svengau commented Jan 4, 2019

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 });

@jpaas
Copy link

jpaas commented Jan 30, 2019

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
Copy link

jpaas commented Jan 30, 2019

Oh BTW this guy seems to have found a workaround for not overwriting with defaults: awslabs/aws-mobile-appsync-sdk-js#195 (comment)

@mbrowne
Copy link

mbrowne commented Jan 30, 2019

apollo-link-state is in the process of being integrated into apollo-client, including multiple bug fixes and new features. For more info, see apollographql/apollo-client#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.

@maziarz
Copy link

maziarz commented Feb 18, 2019

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

@fozzarelo
Copy link

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.

@mbrowne
Copy link

mbrowne commented Mar 9, 2019

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.

@adrienharnay
Copy link

@peggyrayzis could we move this to the apollo repository?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests