New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apollo Client local state #4155

Closed
wants to merge 157 commits into
base: master
from

Conversation

Projects
None yet
7 participants
@hwillson
Copy link
Member

hwillson commented Nov 19, 2018

In a nutshell, this PR migrates the local state handling capabilities provided by apollo-link-state, into the Apollo Client core. @client directive handling is fully managed by Apollo Client (through the QueryManager), without requiring a separate Apollo Link.

Please note: Apollo Client local state functionality is in an alpha state. We're still considering changes, which might affect the public API.

While opinionated, these changes are being considered to help address several outstanding apollo-link-state issues that are difficult to address in a link chain, and open the door to future Apollo Client changes that integrate local state handling more closely with the Apollo Cache.

The implementation outlined in this PR closely mirrors existing apollo-link-state functionality, with a few changes / additions to make handling local state with Apollo Client more flexible and easy to use.

Key Changes

  • It is no longer necessary to add apollo-link-state to your link chain. Apollo Client includes all local state functionality out of the box.

  • The idea of initializing your local cache with apollo-link-state's fixed defaults option has been replaced with a new initializer function approach. Initializers are simple functions, that can be run during ApolloClient instantiation, or via the ApolloClient.runInitializers function. These functions are mapped against the name of the field you want to update in the cache, and the result of running the function is stored in the cache against that field. Initializers can be as flexible as you need them to be, and can either write the returned result directly to the cache, or be configured to skip writing to the cache, letting you handle that manually inside the initializer function yourself. Each initializer function is called with access to the ApolloClient instance, so the full AC API is available.

  • Initializers do not check to see if they'll overwrite existing data when run. For now we're avoiding the extra cache hit of having initializers check to see if data already exists for a field, before overwriting it. This functionality can be overwritten by manually checking and updating the cache yourself in an initializer function.

  • Initializers are only run once (to help prevent overwriting data accidentally).

  • Local resolvers can be set through the ApolloClient resolvers constructor param, or by calling the ApolloClient.addResolvers function.

  • A local schema can be defined by passing it into the ApolloClient constructor via the typeDefs parameter.

  • Mixing remote and local fields together in queries / selection sets (aka virtual fields) is still supported, along with some additional features. @client ... @export(as: "someVar") can now be used to pass the result of a locally resolved query in as a variable for a remote query, all in the same request. For example:

query authorPosts($authorId: Int!) {                                          
  currentAuthor @client {                                                     
    id @export(as: "authorId")                                                
    firstName                                                                 
  }                                                                           
                                                                              
  author(id: $authorId) {                                                     
    id                                                                        
    firstName                                                                 
    posts {                                                                   
      title                                                                   
      votes                                                                   
    }                                                                         
  } 
}
  • Subscriptions now recognize the @client directive, meaning locally resolved data can be combined with incoming subscription data.

  • Full SSR support.

  • Numerous bug fixes and changes made to address outstanding apollo-link-state issues (issues labelled as ac-local-state-ready are fixed by these changes).

  • And more! 馃檪

This PR is still a work in progress (and is not ready for review), but we're pushing hard to get it wrapped up soon.

TODO:

Documentation. This PR is already getting pretty large, so I'll submit the updated local state docs in a separate PR.

hwillson added some commits Oct 4, 2018

chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.0
 - apollo-cache-inmemory@1.3.1-local-state-alpha.0
 - apollo-cache@1.1.18-local-state-alpha.0
 - apollo-client@2.4.3-local-state-alpha.0
 - apollo-utilities@1.0.22-local-state-alpha.0
 - graphql-anywhere@4.1.20-local-state-alpha.0
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.1
 - apollo-cache-inmemory@1.3.1-local-state-alpha.1
 - apollo-cache@1.1.18-local-state-alpha.1
 - apollo-client@2.4.3-local-state-alpha.1
 - apollo-utilities@1.0.22-local-state-alpha.1
 - graphql-anywhere@4.1.20-local-state-alpha.1
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.2
 - apollo-cache-inmemory@1.3.1-local-state-alpha.2
 - apollo-cache@1.1.18-local-state-alpha.2
 - apollo-client@2.4.3-local-state-alpha.2
 - apollo-utilities@1.0.22-local-state-alpha.2
 - graphql-anywhere@4.1.20-local-state-alpha.2
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.3
 - apollo-cache-inmemory@1.3.1-local-state-alpha.3
 - apollo-cache@1.1.18-local-state-alpha.3
 - apollo-client@2.4.3-local-state-alpha.3
 - apollo-utilities@1.0.22-local-state-alpha.3
 - graphql-anywhere@4.1.20-local-state-alpha.3
Re-arrange existing initializer check
We want to make sure brand new initializer functions, that are
associated with a field that is already in the initializer list,
can be overwritten by new initializer functions.
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.4
 - apollo-cache-inmemory@1.3.1-local-state-alpha.4
 - apollo-cache@1.1.18-local-state-alpha.4
 - apollo-client@2.4.3-local-state-alpha.4
 - apollo-utilities@1.0.22-local-state-alpha.4
 - graphql-anywhere@4.1.20-local-state-alpha.4
Temporarily disabling `graphql-anywhere` async rollup
The `graphql-async` rollup config is currently doing strange
things in the local state alpha. The plan is to completely replace
the current non-async version of `graphql` with the async version,
which will fix this issue. Until this happens, we'll push a
sync `graphql` version alongside the async `graphqlAsync` version,
to let alpha testing continue.
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.5
 - apollo-cache-inmemory@1.3.1-local-state-alpha.5
 - apollo-cache@1.1.18-local-state-alpha.5
 - apollo-client@2.4.3-local-state-alpha.5
 - apollo-utilities@1.0.22-local-state-alpha.5
 - graphql-anywhere@4.1.20-local-state-alpha.5
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.6
 - apollo-cache-inmemory@1.3.1-local-state-alpha.6
 - apollo-cache@1.1.18-local-state-alpha.6
 - apollo-client@2.4.3-local-state-alpha.6
 - apollo-utilities@1.0.22-local-state-alpha.6
 - graphql-anywhere@4.1.20-local-state-alpha.6
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.7
 - apollo-cache-inmemory@1.3.1-local-state-alpha.7
 - apollo-cache@1.1.18-local-state-alpha.7
 - apollo-client@2.4.3-local-state-alpha.7
 - apollo-utilities@1.0.22-local-state-alpha.7
 - graphql-anywhere@4.1.20-local-state-alpha.7
Adjust subscriber completion to wait for local state result
The graphql-anywhere `graphql` function used by local resolvers
is async, which means we need to make sure it has completed,
before finalizing the fetchRequest observable subscriber
function. Otherwise, the observable can be marked as complete,
while local resolver data is still coming in (and will then be
dropped).
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.8
 - apollo-cache-inmemory@1.3.1-local-state-alpha.8
 - apollo-cache@1.1.18-local-state-alpha.8
 - apollo-client@2.4.3-local-state-alpha.8
 - apollo-utilities@1.0.22-local-state-alpha.8
 - graphql-anywhere@4.1.20-local-state-alpha.8
Store initialization
Change focus from initializing the store with defaults to
accommodate local state handling, to a more broad and flexible
store initialization strategy, that can be used outside of local
state handling.

This commit:

- Renames across the board to accommodate the newer store
  initialization approach
- Moves store initialization into `ApolloClient`
- Makes sure store initializer functions get access to the
  current `ApolloClient` instance
Accommodate @client(as: "someVarName") support
Data loaded from the cache using `@client` can now be stored
in a variable, and that variable can then be used in associated
queries. For exammple:

```
query topPosts($postIds: [Int]!) {
  topPosts @client(as: "postIds")
  posts(ids: $postIds) {
    title
    votes
  }
}
```

Top post IDs are loaded from the cache, and made available to
the `posts` query as the `postIds` variable.
Stop storing full store initializer functions
We don't really need to store the full store initializer functions.
We just need a way of making sure initializers are only run once.
We can do this by just tracking the field name associated with the
initializer. In the future we might want to store full initializer
functions, to say re-trigger them automatically after a
`resetStore` call, but this could prove to be more hassle than its
worth. Re-triggering initializer functions can always be handled
in userland after a `resetStore` (using `onResetStore`). Also,
if certain initializers are triggered via certain code split
paths, then re-triggering that initializer again automatically in
a different part of the application could be tricky/dangerous.

This commit also moves the bulk of the initializer handling out
of the main AC class, storing it alongside the `DataStore` it's
impacting.
chore: Publish
 - apollo-boost@0.1.17-local-state-alpha.9
 - apollo-cache-inmemory@1.3.1-local-state-alpha.9
 - apollo-cache@1.1.18-local-state-alpha.9
 - apollo-client@2.4.3-local-state-alpha.9
 - apollo-utilities@1.0.22-local-state-alpha.9
 - graphql-anywhere@4.1.20-local-state-alpha.9

hwillson added some commits Jan 14, 2019

chore: Publish
 - apollo-boost@0.3.0-alpha.10
 - apollo-cache-inmemory@1.4.0-alpha.10
chore: Publish
 - apollo-boost@0.3.0-alpha.11
 - apollo-cache-inmemory@1.4.0-alpha.11
 - apollo-client@2.5.0-alpha.9
Make sure `removeClientSetsFromDocument` can handle fragments
This commit updates the `apollo-utilities` transformation
`nullIfDocIsEmpty` helper function to handle both
`OperationDefinition`'s and `FragmentDefinition`'s.
chore: Publish
 - apollo-boost@0.3.0-alpha.12
 - apollo-cache-inmemory@1.4.0-alpha.12
 - apollo-cache@1.2.0-alpha.9
 - apollo-client@2.5.0-alpha.10
 - apollo-utilities@1.1.0-alpha.10
 - graphql-anywhere@4.2.0-alpha.9

@hwillson hwillson referenced this pull request Jan 14, 2019

Open

React Hooks support #2539

@hwillson

This comment has been minimized.

Copy link
Member Author

hwillson commented Jan 14, 2019

Just a quick note about the bundle size increase here - it's temporary, and will be pruned down shortly.

hwillson added some commits Jan 15, 2019

@alidcastano

This comment has been minimized.

Copy link

alidcastano commented Jan 16, 2019

@hwillson fyi - I'm trying this locally in react-native and the data turns undefined unless I initialize it myself before querying, i.e. writeData({ data: { isLoggedIn: true } }) (which im totally fine with, but figured I'd let you know)

the tutorial online did not have setup intructions but from looking at PR, pretty sure im passing the options correctly so not sure why the initializers aren't working

new ApolloClient({
    ...
    typeDefs: gql`
     extend type Query {
      isLoggedIn: Boolean!
    }
   `,
    initializers: {
      isLoggedIn: () => false
   }
  })
}

also, will initializers support async functions? It'll be necessary in order for the isLoggedIn example to work with react-native since theAsyncStorage methods return promises

@alidcastano

This comment has been minimized.

Copy link

alidcastano commented Jan 16, 2019

another issue Im seeing is that for the first query, the data is not available and returns an empty object first and then the result, i.e. {} then {isLoggedIn: true} which causes the need for unnecessary loading/conditionals so the wrong view is not rendered (but this could perhaps be related to above initializer configuration not working)

(if you're not seeking early testing right now, apologies and ignore these comments!)

@hwillson

This comment has been minimized.

Copy link
Member Author

hwillson commented Jan 17, 2019

@alidcastano We're definitely seeking feedback, so thanks for this!

Initializers called during ApolloClient instantiation are run synchronously, so the cache should be prepped before your first call against it. Your code sample looks right, and you shouldn't have to call a writeData yourself. Can you confirm which exact version of apollo-client@alpha you have installed?

Regarding initializers supporting async, they do currently, but not via the ApolloClient constructor. Initializers called through the constructor run synchronously, so we can make sure the cache is prepped in the expected state before a query is fired against it. ApolloClient's runInitializers method supports async though - e.g.:

const client = new ApolloClient({ ... });
...
client.runInitializers(async () => {
  await someFunction();
  ...
});
@alidcastano

This comment has been minimized.

Copy link

alidcastano commented Jan 17, 2019

hmm for some reason the intializers weren't working before but now they are 馃憤

in regards to the async functions, passing a function to runInitializers didn't work, and I couldn't find support for it in the code or tests from this PR

but I was able to get it working using the following code

// schema.ts
...
const initializers = (ctx) => ({
  isLoggedIn: () => !!(auth.getToken(ctx && ctx.req))
})

// index.ts
function createClient(initialState = null, ctx = null) {
    ...
  const client = new ApolloClient({ ...., typeDefs })

   // note: only run initializers / return promise if there's no initial state
  // so that we don't have to wait when rehydrating client 
   if (!initialState) {
    return client
      .runInitializers(initializers(ctx))
      .then(() => client)
  }
  return client 
}

(^^ I'm guessing we don't need to pass the initializes as a constructor option if we're running them ourselves)

my project is a react native app and a server-rendered web app (using react-native-web), so i can confirm in works on both environments!

@chaffeqa

This comment has been minimized.

Copy link

chaffeqa commented Jan 19, 2019

FYI we just cut over to try this out, seems to be working fine!

Using SSR and web client-side (for PWA support)

@hwillson

This comment has been minimized.

Copy link
Member Author

hwillson commented Jan 19, 2019

Quick note - this PR has been replaced by #4338. We'll continue the discussion there. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment