apollographql / apollo-link-state

✨ Manage your application's state with Apollo!
MIT License
1.4k stars 101 forks source link

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

Open isopterix opened 6 years ago

isopterix commented 6 years ago

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 commented 6 years ago

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

raeesaa commented 6 years ago

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

peggyrayzis commented 6 years ago

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

isopterix commented 6 years ago

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

benseitz commented 6 years ago

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

isopterix commented 6 years ago

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 commented 6 years ago

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

aaronp-hd commented 6 years ago

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

craigmulligan commented 6 years ago

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

ramakrishnamundru commented 6 years ago

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

pawelsamsel commented 6 years ago

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 commented 6 years ago

Same for me :(

mbrowne commented 6 years ago

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

svengau commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

mbrowne commented 5 years ago

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.

maziarz commented 5 years ago

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

fozzarelo commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

@peggyrayzis could we move this to the apollo repository?