Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add client side support for @defer directive #3686

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
20c88eb
Pagination: fix typo
dandv Jul 18, 2018
71e5774
docs: Update link to `create-react-app` instructions.
abernix Jul 18, 2018
61bd8a6
Merge deferred patches with the initial ExecutionResult
clarencenpy Jul 13, 2018
806dec6
Expose a loadingState tree for deferred queries
clarencenpy Jul 17, 2018
df030b8
Improved DX by exposing a proxied version of the loadingState tree
clarencenpy Jul 17, 2018
fbf74f7
Deep freeze all fields on resultFromStore except loadingState
clarencenpy Jul 17, 2018
195e779
Improved DX, omit `_isLoaded` from the access path
clarencenpy Jul 17, 2018
b578bf6
Handle inline fragments in query
clarencenpy Jul 17, 2018
7238f05
Remove usage of Proxy and just return a compacted loadingState tree
clarencenpy Jul 17, 2018
ab3a3bd
Use experimental release of `apollo-link-dedup`
clarencenpy Jul 17, 2018
b6f32dd
Handle FragmentSpreads in the query
clarencenpy Jul 18, 2018
aafe10b
Always treat patches returning a different result
clarencenpy Jul 18, 2018
b4069cb
Handle fields on fragment spreads that already exist in selectionSet
clarencenpy Jul 19, 2018
dc56ccb
Pass a flag to indicated deferred queries to the link stack
clarencenpy Jul 19, 2018
0dbdb34
Pin package dependencies to the alpha versions with defer support
clarencenpy Jul 19, 2018
48115e3
Changelog updates
hwillson Jul 19, 2018
a0c57f6
Changelog updates
hwillson Jul 19, 2018
b4b205d
chore: Publish
hwillson Jul 19, 2018
4f62157
Slight changes to make alpha publishing easier
hwillson Jul 19, 2018
00dccf3
chore: Publish
hwillson Jul 19, 2018
a9483b1
Made changes based on comments received
clarencenpy Jul 20, 2018
b72cf3e
Initialize loadingState correctly when an array is returned in the in…
clarencenpy Jul 21, 2018
01b8e0d
chore: Publish
clarencenpy Jul 23, 2018
7be7bc4
Add client docs for @defer
clarencenpy Jul 24, 2018
5f1c6e0
Set `returnPartialData` to be true for deferred queries
clarencenpy Jul 24, 2018
4f5fc85
Null check
clarencenpy Jul 25, 2018
891c21b
chore: Publish
clarencenpy Jul 25, 2018
0b31c04
Update dependencies
clarencenpy Jul 25, 2018
bdb248f
Add some info about patches that get streamed in behind the scenes
clarencenpy Jul 25, 2018
9460816
chore: Publish
clarencenpy Jul 25, 2018
39cf4fb
Publish with exact versioning
clarencenpy Jul 25, 2018
dcd0a37
Update client docs
clarencenpy Jul 26, 2018
d6b50db
chore: Publish
clarencenpy Jul 26, 2018
fa240ec
Update dependencies
clarencenpy Jul 27, 2018
502b2c4
chore: Publish
clarencenpy Jul 27, 2018
33ad6c1
Update NetworkStatus to reflect partial state
clarencenpy Jul 31, 2018
a5c9b28
Merge branch 'master' into defer-support
clarencenpy Jul 31, 2018
148b7ec
Refactored to return correct loadingState on complete
clarencenpy Jul 31, 2018
de2cdfb
Initialize loadingState when reading deferred query from cache
clarencenpy Jul 31, 2018
893ead8
chore: Publish
clarencenpy Jul 31, 2018
e995d19
Make sure that loadingState is in sync with data read from cache
clarencenpy Aug 2, 2018
701e20c
Set test environment to 'node'
clarencenpy Aug 3, 2018
6f633da
Broadcasting changes from cache should update loadingState
clarencenpy Aug 8, 2018
f11e9a6
Merge branch 'master' into defer-support
clarencenpy Aug 8, 2018
3cbfc09
Remove `console.info` for each patch
clarencenpy Aug 8, 2018
41b4b8c
Update CHANGELOG
clarencenpy Aug 8, 2018
dc53866
chore: Publish
clarencenpy Aug 8, 2018
77e0c0c
Update defer docs with how to do preloading
clarencenpy Aug 8, 2018
d904386
Add type for `loadingState`
clarencenpy Aug 9, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

### Apollo Client (vNext)

- Added `@defer` support [#3686](https://github.com/apollographql/apollo-client/pull/3686)
- Adjusted the `graphql` peer dependency to cover explicit minor ranges.
Since the ^ operator only covers any minor version if the major version
is not 0 (since a major version of 0 is technically considered development by
Expand Down
87 changes: 75 additions & 12 deletions docs/source/features/defer-support.md
Expand Up @@ -5,7 +5,7 @@ description: Optimize data loading with the @defer directive

<h2 id="defer-setup">Setting up</h2>

Note: `@defer` support is an experimental feature that is only available in the alpha preview of Apollo Server and Apollo Client.
> Note: `@defer` support is an **experimental feature** that is only available in alpha versions of Apollo Server and Apollo Client.

- On the server:

Expand All @@ -19,7 +19,7 @@ Note: `@defer` support is an experimental feature that is only available in the
```
Or if you are using Apollo Client:
```
npm install apollo-client@alpha apollo-cache-inmemory@alpha apollo-link-http@alpha apollo-link-error apollo-link
npm install apollo-client@alpha apollo-cache-inmemory@alpha apollo-link-http@alpha apollo-link-error apollo-link react-apollo@alpha
```

<h2 id="defer">The `@defer` Directive</h2>
Expand All @@ -38,15 +38,19 @@ As an example, take a look at the following query that populates a NewsFeed page
query NewsFeed {
newsFeed {
stories {
text
id
title
comments {
id
text
}
}
recommendedForYou {
story {
text
id
title
comments {
id
text
}
}
Expand All @@ -66,15 +70,19 @@ We can optimize the above query with `@defer`:
query NewsFeed {
newsFeed {
stories {
text
id
title
comments @defer {
id
text
}
}
recommendedForYou @defer {
story {
text
id
title
comments @defer {
id
text
}
}
Expand All @@ -91,7 +99,7 @@ Once you have added `@defer`, Apollo Server will return an initial response with
{
"data": {
"newsFeed": {
"stories": [{ "text": "...", "comments": null }],
"stories": [{ "id": "...", "title": "...", "comments": null }],
"recommendedForYou": null
}
}
Expand All @@ -105,7 +113,9 @@ Once you have added `@defer`, Apollo Server will return an initial response with
"data": [
{
"story": {
"text": "..."
"id": "...",
"title": "...",
"comments": null
},
"matchScore": 99
}
Expand Down Expand Up @@ -176,12 +186,12 @@ You can use it in a React component like this:
```graphql
fragment StoryDetail on Story {
id
text
title
}
query {
newsFeed {
stories {
text @defer
title @defer
...StoryDetail
}
}
Expand All @@ -199,8 +209,61 @@ There is no additional setup for the transport required to use `@defer`. By defa

`@defer` is one of those features that work best if used in moderation. If it is used too granularly (on many nested fields), the overhead of performing patching and re-rendering could be worse than just waiting for the full query to resolve. Try to limit `@defer` to fields that take a significantly longer time to load. This is super easy to figure out if you have Apollo Engine set up!

<h2 id="defer-preloading">Preloading Data with `@defer`</h2>

Another super useful pattern for using `@defer` is preloading data that will be required in subsequent views. For illustration, imagine that each story has a `text` field that takes a long time to load. `text` is not required when we load the newsfeed view - we only need it to show the story detail view, which makes a query like this:

```graphql
query StoryDetail($id: ID!) {
story(id: $id) {
id
title
text @defer
comments @defer {
id
text
}
}
}
```

However, instead for waiting for the user to navigate to the story detail view before firing that query, we could add `text` as a deferred field when we first load the newsfeed. This will allow `text` to preload in the background for all the stories.

```graphql
query NewsFeed {
newsFeed {
stories {
id
title
text @defer # Not needed now, but preload it first
comments @defer {
id
text
}
}
}
}
```

Then, we will need to set up a [cache redirect](https://www.apollographql.com/docs/react/advanced/caching.html#cacheRedirect) to tell Apollo Client where to look for cached data for the `StoryDetail` query.

```javascript
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cacheRedirects: {
Query: {
story: (_, { id }, { getCacheKey }) =>
getCacheKey({ __typename: 'Story', id }),
},
},
});
```

Now, when the user navigates to each story detail view, it will load instantly as the data required is already fetched and stored in the cache.


<h2 id="defer-servers">Use with other GraphQL servers</h2>

If you are sending queries to a GraphQL server that does not support `@defer`, it is likely that the `@defer` directive is simply ignored, or a GraphQL validation error is thrown.
If you are sending queries to a GraphQL server that does not support `@defer`, it is likely that the `@defer` directive is simply ignored, or a GraphQL validation error is thrown: `Unknown directive "defer"`

If you would want to implement a GraphQL server that is able to interoperate with Apollo Client, please look at the documentation [here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md).
To implement a GraphQL server that will interoperate with Apollo Client for `@defer` support, please look at the [specification here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md).
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -18,7 +18,8 @@
"coverage": "lerna run -- coverage",
"coverage:upload": "codecov",
"danger": "danger run --verbose",
"deploy": "lerna publish -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy"
"deploy": "lerna publish -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy",
"deploy-alpha": "lerna publish --npm-tag alpha -m \"chore: Publish\" --independent --exact && cd packages/apollo-client && npm run deploy"
},
"bundlesize": [
{
Expand Down Expand Up @@ -53,6 +54,7 @@
}
],
"jest": {
"testEnvironment": "node",
"transform": {
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
Expand Down
12 changes: 6 additions & 6 deletions packages/apollo-boost/package.json
@@ -1,6 +1,6 @@
{
"name": "apollo-boost",
"version": "0.1.12",
"version": "0.2.0-alpha.14",
"description": "The easiest way to get started with Apollo Client",
"author": "Peggy Rayzis <peggy@apollographql.com>",
"contributors": [
Expand Down Expand Up @@ -34,19 +34,19 @@
"filesize": "npm run build && npm run build:browser"
},
"dependencies": {
"apollo-cache": "^1.1.13",
"apollo-cache-inmemory": "^1.2.6",
"apollo-client": "^2.3.7",
"apollo-cache": "1.1.13-alpha.14",
"apollo-cache-inmemory": "1.2.6-alpha.14",
"apollo-client": "2.4.0-alpha.14",
"apollo-link": "^1.0.6",
"apollo-link-error": "^1.0.3",
"apollo-link-http": "^1.3.1",
"apollo-link-http": "alpha",
"apollo-link-state": "^0.4.0",
"graphql-tag": "^2.4.2"
},
"devDependencies": {
"@types/graphql": "0.12.7",
"@types/jest": "22.2.3",
"apollo-utilities": "^1.0.17",
"apollo-utilities": "1.0.17-alpha.14",
"browserify": "15.2.0",
"fetch-mock": "6.5.2",
"graphql": "0.13.2",
Expand Down
8 changes: 4 additions & 4 deletions packages/apollo-cache-inmemory/package.json
@@ -1,6 +1,6 @@
{
"name": "apollo-cache-inmemory",
"version": "1.2.6",
"version": "1.2.6-alpha.14",
"description": "Core abstract of Caching layer for Apollo Client",
"author": "James Baxley <james@meteor.com>",
"contributors": [
Expand Down Expand Up @@ -40,9 +40,9 @@
"filesize": "npm run build:browser"
},
"dependencies": {
"apollo-cache": "^1.1.13",
"apollo-utilities": "^1.0.17",
"graphql-anywhere": "^4.1.15"
"apollo-cache": "1.1.13-alpha.14",
"apollo-utilities": "1.0.17-alpha.14",
"graphql-anywhere": "4.1.15-alpha.14"
},
"peerDependencies": {
"graphql": "0.11.7 || ^0.12.0 || ^0.13.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-cache-inmemory/src/readFromStore.ts
Expand Up @@ -16,11 +16,11 @@ import {
import { Cache } from 'apollo-cache';

import {
ReadQueryOptions,
IdValueWithPreviousResult,
ReadStoreContext,
DiffQueryAgainstStoreOptions,
StoreObject,
ReadQueryOptions,
} from './types';

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/apollo-cache/package.json
@@ -1,6 +1,6 @@
{
"name": "apollo-cache",
"version": "1.1.13",
"version": "1.1.13-alpha.14",
"description": "Core abstract of Caching layer for Apollo Client",
"author": "James Baxley <james@meteor.com>",
"contributors": [
Expand Down Expand Up @@ -39,7 +39,7 @@
"filesize": "npm run build && npm run build:browser"
},
"dependencies": {
"apollo-utilities": "^1.0.17"
"apollo-utilities": "1.0.17-alpha.14"
},
"devDependencies": {
"@types/graphql": "0.12.7",
Expand Down
4 changes: 1 addition & 3 deletions packages/apollo-cache/src/cache.ts
Expand Up @@ -9,9 +9,7 @@ export type Transaction<T> = (c: ApolloCache<T>) => void;
export abstract class ApolloCache<TSerialized> implements DataProxy {
// required to implement
// core API
public abstract read<T, TVariables = any>(
query: Cache.ReadOptions<TVariables>,
): T | null;
public abstract read<T, TVariables = any>(query: Cache.DiffOptions): T | null;
public abstract write<TResult = any, TVariables = any>(
write: Cache.WriteOptions<TResult, TVariables>,
): void;
Expand Down
10 changes: 5 additions & 5 deletions packages/apollo-client/package.json
@@ -1,7 +1,7 @@
{
"name": "apollo-client",
"private": true,
"version": "2.3.7",
"version": "2.4.0-alpha.14",
"description": "A simple yet functional GraphQL client.",
"main": "./lib/bundle.umd.js",
"module": "./lib/index.js",
Expand Down Expand Up @@ -47,10 +47,10 @@
"license": "MIT",
"dependencies": {
"@types/zen-observable": "^0.8.0",
"apollo-cache": "^1.1.13",
"apollo-cache": "1.1.13-alpha.14",
"apollo-link": "^1.0.0",
"apollo-link-dedup": "^1.0.0",
"apollo-utilities": "^1.0.17",
"apollo-link-dedup": "alpha",
"apollo-utilities": "1.0.17-alpha.14",
"symbol-observable": "^1.0.2",
"zen-observable": "^0.8.0"
},
Expand All @@ -65,7 +65,7 @@
"@types/jest": "22.2.3",
"@types/lodash": "4.14.116",
"@types/node": "10.5.6",
"apollo-cache-inmemory": "^1.2.6",
"apollo-cache-inmemory": "1.2.6-alpha.14",
"benchmark": "2.1.4",
"browserify": "15.2.0",
"bundlesize": "0.17.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-client/scripts/deploy.sh
Expand Up @@ -59,4 +59,4 @@ cp ../../LICENSE npm/
# flow typings
# cp -R flow-typed npm/

cd npm && npm publish
cd npm && npm publish --tag alpha
33 changes: 28 additions & 5 deletions packages/apollo-client/src/core/ObservableQuery.ts
@@ -1,10 +1,10 @@
import {
isEqual,
tryFunctionOrLogError,
maybeDeepFreeze,
tryFunctionOrLogError,
} from 'apollo-utilities';
import { GraphQLError } from 'graphql';
import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus';
import { isNetworkRequestInFlight, NetworkStatus } from './networkStatus';
import { Observable, Observer, Subscription } from '../util/Observable';

import { QueryScheduler } from '../scheduler/scheduler';
Expand All @@ -14,15 +14,16 @@ import { ApolloError } from '../errors/ApolloError';
import { QueryManager } from './QueryManager';
import { ApolloQueryResult, FetchType, OperationVariables } from './types';
import {
ModifiableWatchQueryOptions,
WatchQueryOptions,
ErrorPolicy,
FetchMoreQueryOptions,
ModifiableWatchQueryOptions,
SubscribeToMoreOptions,
ErrorPolicy,
UpdateQueryFn,
WatchQueryOptions,
} from './watchQueryOptions';

import { QueryStoreValue } from '../data/queries';
import { hasDirectives } from 'apollo-utilities';

export type ApolloCurrentResult<T> = {
data: T | {};
Expand All @@ -31,6 +32,17 @@ export type ApolloCurrentResult<T> = {
networkStatus: NetworkStatus;
error?: ApolloError;
partial?: boolean;
loadingState?: T | {};
// loadingState is exposed to the client for deferred queries, with a shape
// that mirrors that of the data, but instead of the leaf nodes being
// GraphQLOutputType, it is (undefined | boolean).
// Right now, we have not accounted for this difference, but I think it is
// still usable in the context of checking for the presence of a field.
//
// TODO: Additional work needs to be done in `apollo-codegen-core` to generate
// a separate type for the loadingState, which will then be passed in as
// follows - ApolloCurrentResult<T, LoadingStateType>
// Open issue here: https://github.com/apollographql/apollo-cli/issues/539
};

export interface FetchMoreOptions<
Expand Down Expand Up @@ -218,6 +230,17 @@ export class ObservableQuery<
result.errors = queryStoreValue.graphQLErrors;
}

if (queryStoreValue) {
result.loadingState = queryStoreValue.compactedLoadingState;
} else {
if (hasDirectives(['defer'], this.options.query)) {
// Make sure that we have loadingState for deferred queries
// If the queryStore has not been initialized, set loading to true and
// wait for the next update.
result.loading = true;
}
}

if (!partial) {
const stale = false;
this.lastResult = { ...result, stale };
Expand Down