-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Imperative read and write methods #1310
Changes from 27 commits
03f7f76
f5f2ba6
c1acb42
f496df2
d246ca5
bce014a
fec7003
33d60fe
edfadae
bd6a00f
2bc3457
baff038
37cd7b9
b716e19
793c59d
116f9b0
23eb614
d3b3995
dc915ec
f56d476
aa182fa
72ad7c6
b2b9ae7
2e965a2
3f57b27
238f883
312f96a
e108dde
e69c858
c42b0c2
3c7f401
f2a6fc7
97686e3
4103831
c6335a6
d11a4b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ import { | |
SelectionSetNode, | ||
/* tslint:enable */ | ||
|
||
DocumentNode, | ||
FragmentDefinitionNode, | ||
} from 'graphql'; | ||
|
||
import { | ||
|
@@ -58,6 +60,16 @@ import { | |
storeKeyNameFromFieldNameAndArgs, | ||
} from './data/storeUtils'; | ||
|
||
import { | ||
getFragmentQueryDocument, | ||
} from './queries/getFromAST'; | ||
|
||
import { | ||
DataProxy, | ||
ReduxDataProxy, | ||
TransactionDataProxy, | ||
} from './data/proxy'; | ||
|
||
import { | ||
version, | ||
} from './version'; | ||
|
@@ -100,6 +112,8 @@ export default class ApolloClient { | |
public queryDeduplication: boolean; | ||
|
||
private devToolsHookCb: Function; | ||
private optimisticWriteId: number; | ||
private proxy: DataProxy | undefined; | ||
|
||
/** | ||
* Constructs an instance of {@link ApolloClient}. | ||
|
@@ -232,6 +246,8 @@ export default class ApolloClient { | |
} | ||
|
||
this.version = version; | ||
|
||
this.optimisticWriteId = 1; | ||
} | ||
|
||
/** | ||
|
@@ -336,6 +352,219 @@ export default class ApolloClient { | |
return this.queryManager.startGraphQLSubscription(realOptions); | ||
} | ||
|
||
/** | ||
* Tries to read some data from the store in the shape of the provided | ||
* GraphQL query without making a network request. This method will start at | ||
* the root query. To start at a specific id returned by `dataIdFromObject` | ||
* use `readFragment`. | ||
* | ||
* @param query The GraphQL query shape to be used. | ||
* | ||
* @param variables Any variables that the GraphQL query may depend on. | ||
*/ | ||
public readQuery<QueryType>( | ||
query: DocumentNode, | ||
variables?: Object, | ||
): QueryType { | ||
return this.initProxy().readQuery<QueryType>(query, variables); | ||
} | ||
|
||
/** | ||
* Tries to read some data from the store in the shape of the provided | ||
* GraphQL fragment without making a network request. This method will read a | ||
* GraphQL fragment from any arbitrary id that is currently cached, unlike | ||
* `readQuery` which will only read from the root query. | ||
* | ||
* You must pass in a GraphQL document with a single fragment or a document | ||
* with multiple fragments that represent what you are reading. If you pass | ||
* in a document with multiple fragments then you must also specify a | ||
* `fragmentName`. | ||
* | ||
* @param id The root id to be used. This id should take the same form as the | ||
* value returned by your `dataIdFromObject` function. If a value with your | ||
* id does not exist in the store, `null` will be returned. | ||
* | ||
* @param fragment A GraphQL document with one or more fragments the shape of | ||
* which will be used. If you provide more then one fragments then you must | ||
* also specify the next argument, `fragmentName`, to select a single | ||
* fragment to use when reading. | ||
* | ||
* @param fragmentName The name of the fragment in your GraphQL document to | ||
* be used. Pass `undefined` if there is only one fragment and you want to | ||
* use that. | ||
* | ||
* @param variables Any variables that your GraphQL fragments depend on. | ||
*/ | ||
public readFragment<FragmentType>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just out of curiosity: what error gets thrown if you provide an id that doesn't exist in the store? I have a hunch that we might want to improve that error message for this function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. It would return an empty object because the fragment pattern matching wouldn’t match anything. I made it return |
||
id: string, | ||
fragment: DocumentNode, | ||
fragmentName?: string, | ||
variables?: Object, | ||
): FragmentType | null { | ||
return this.initProxy().readFragment<FragmentType>(id, fragment, fragmentName, variables); | ||
} | ||
|
||
/** | ||
* Writes some data in the shape of the provided GraphQL query directly to | ||
* the store. This method will start at the root query. To start at a a | ||
* specific id returned by `dataIdFromObject` then use `writeFragment`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's an extra "a" and "then". |
||
* | ||
* @param data The data you will be writing to the store. | ||
* | ||
* @param query The GraphQL query shape to be used. | ||
* | ||
* @param variables Any variables that the GraphQL query may depend on. | ||
*/ | ||
public writeQuery( | ||
data: any, | ||
query: DocumentNode, | ||
variables?: Object, | ||
): void { | ||
return this.initProxy().writeQuery(data, query, variables); | ||
} | ||
|
||
/** | ||
* Writes some data in the shape of the provided GraphQL fragment directly to | ||
* the store. This method will write to a GraphQL fragment from any arbitrary | ||
* id that is currently cached, unlike `writeQuery` which will only write | ||
* from the root query. | ||
* | ||
* You must pass in a GraphQL document with a single fragment or a document | ||
* with multiple fragments that represent what you are writing. If you pass | ||
* in a document with multiple fragments then you must also specify a | ||
* `fragmentName`. | ||
* | ||
* @param data The data you will be writing to the store. | ||
* | ||
* @param id The root id to be used. This id should take the same form as the | ||
* value returned by your `dataIdFromObject` function. | ||
* | ||
* @param fragment A GraphQL document with one or more fragments the shape of | ||
* which will be used. If you provide more then one fragments then you must | ||
* also specify the next argument, `fragmentName`, to select a single | ||
* fragment to use when reading. | ||
* | ||
* @param fragmentName The name of the fragment in your GraphQL document to | ||
* be used. Pass `undefined` if there is only one fragment and you want to | ||
* use that. | ||
* | ||
* @param variables Any variables that your GraphQL fragments depend on. | ||
*/ | ||
public writeFragment( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we feel about making this take keyword arguments instead of 5 positional arguments? Long lists of positional arguments are fine in TS/Flow but I find them very hard to use in JS. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They were named at first, I just really like the look of: client.readQuery(gql`{ ... }`);
client.writeQuery({ ... }, gql`{ ... }`); vs. client.readQuery({
query: gql`{ ... }`,
});
client.writeQuery({
data: { ... },
query: gql`{ ... }`,
}); and so I decided to make everything positional instead of named. I don’t actually think it is that bad with // Common
client.readFragment(
'Todo42',
gql`fragment todo on Todo { text, completed }`,
);
client.writeFragment(
{ text: 'Clean up', completed: false },
'Todo42',
gql`fragment todo on Todo { text, completed }`,
);
// Uncommon
client.readFragment(
'Todo42',
gql`
fragment todo1 on Todo { ...todo2 @include(if: $isTrue) }
fragment todo2 on Todo { text, completed }
`,
'todo1',
{ isTrue: true },
);
client.writeFragment(
{ text: 'Clean up', completed: false },
'Todo42',
gql`
fragment todo1 on Todo { ...todo2 @include(if: $isTrue) }
fragment todo2 on Todo { text, completed }
`,
'todo1',
{ isTrue: true },
); Most of the time you will only have one fragment and therefore won’t need the I understand the desire for named arguments. I just really like how the 80% case looks with positional arguments 😊. Let me know if you still think named arguments are worth it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can always change it later. However, looking at that one example, can we reorder the args to always have the same order?
I think there are a few heuristics for ordering:
However in some sense the fact that we have to think about ordering is a bit unfortunate. At the end of the day it doesn't matter that much, but this seems like the kind of API I would need to look up in the docs frequently to remember how to call it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer named arguments even if it makes the 80% use-case more verbose, because it makes the code easier to read. Rather than having to guess what each arg represents, you know right away because it's named. |
||
data: any, | ||
id: string, | ||
fragment: DocumentNode, | ||
fragmentName?: string, | ||
variables?: Object, | ||
): void { | ||
return this.initProxy().writeFragment(data, id, fragment, fragmentName, variables); | ||
} | ||
|
||
/** | ||
* Writes some data in the shape of the provided GraphQL query directly to | ||
* the store. This method will start at the root query. To start at a | ||
* specific id returned by `dataIdFromObject` then use | ||
* `writeFragmentOptimistically`. | ||
* | ||
* Unlike `writeQuery`, the data written with this method will be stored in | ||
* the optimistic portion of the cache and so will not be persisted. This | ||
* optimistic write may also be rolled back with the `rollback` function that | ||
* was returned. | ||
* | ||
* @param data The data you will be writing to the store. | ||
* | ||
* @param query The GraphQL query shape to be used. | ||
* | ||
* @param variables Any variables that the GraphQL query may depend on. | ||
*/ | ||
public writeQueryOptimistically( | ||
data: any, | ||
query: DocumentNode, | ||
variables?: Object, | ||
): { | ||
rollback: () => void, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll have to explain in the docs why there's no There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no reason why we could not add a It wouldn’t be efficient, but it would work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, but I'd rather just tell people to do that themselves. |
||
} { | ||
const optimisticWriteId = (this.optimisticWriteId++).toString(); | ||
this.initStore(); | ||
this.store.dispatch({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are these implemented via a direct dispatch, while the others use the proxy? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two reasons:
However, now that you mention it, removing I don’t think anyone would miss these methods if we removed them, and it would reduce complexity. I’m going to go ahead and remove these two methods. If you think there is a case for them we can add them back 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds great to me! So what you're saying is, people can still use optimistic writes from the update callback in a mutation, but there will be no way to do it in a totally standalone way. That works for me for a first pass. The only reason you would need optimistic writes otherwise is if you were using REST to do mutations and wanted to do an optimistic update to your store before refetching or something, but that is a somewhat niche thing and can be added later as a feature. |
||
type: 'APOLLO_WRITE_OPTIMISTIC', | ||
optimisticWriteId, | ||
writes: [{ | ||
rootId: 'ROOT_QUERY', | ||
result: data, | ||
document: query, | ||
variables: variables || {}, | ||
}], | ||
}); | ||
return { | ||
rollback: () => this.store.dispatch({ | ||
type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', | ||
optimisticWriteId, | ||
}), | ||
}; | ||
} | ||
|
||
/** | ||
* Writes some data in the shape of the provided GraphQL fragment directly to | ||
* the store. This method will write to a GraphQL fragment from any | ||
* arbitrary id that is currently cached, unlike `writeQueryOptimistically` | ||
* which will only write from the root query. | ||
* | ||
* You must pass in a GraphQL document with a single fragment or a document | ||
* with multiple fragments that represent what you are writing. If you pass | ||
* in a document with multiple fragments then you must also specify a | ||
* `fragmentName`. | ||
* | ||
* Unlike `writeFragment`, the data written with this method will be stored in | ||
* the optimistic portion of the cache and so will not be persisted. This | ||
* optimistic write may also be rolled back with the `rollback` function that | ||
* was returned. | ||
* | ||
* @param data The data you will be writing to the store. | ||
* | ||
* @param id The root id to be used. This id should take the same form as the | ||
* value returned by your `dataIdFromObject` function. | ||
* | ||
* @param fragment A GraphQL document with one or more fragments the shape of | ||
* which will be used. If you provide more then one fragments then you must | ||
* also specify the next argument, `fragmentName`, to select a single | ||
* fragment to use when reading. | ||
* | ||
* @param fragmentName The name of the fragment in your GraphQL document to | ||
* be used. Pass `undefined` if there is only one fragment and you want to | ||
* use that. | ||
* | ||
* @param variables Any variables that your GraphQL fragments depend on. | ||
*/ | ||
public writeFragmentOptimistically( | ||
data: any, | ||
id: string, | ||
fragment: DocumentNode, | ||
fragmentName?: string, | ||
variables?: Object, | ||
): { | ||
rollback: () => void, | ||
} { | ||
const optimisticWriteId = (this.optimisticWriteId++).toString(); | ||
this.initStore(); | ||
this.store.dispatch({ | ||
type: 'APOLLO_WRITE_OPTIMISTIC', | ||
optimisticWriteId, | ||
writes: [{ | ||
rootId: id, | ||
result: data, | ||
document: getFragmentQueryDocument(fragment, fragmentName), | ||
variables: variables || {}, | ||
}], | ||
}); | ||
return { | ||
rollback: () => this.store.dispatch({ | ||
type: 'APOLLO_WRITE_OPTIMISTIC_ROLLBACK', | ||
optimisticWriteId, | ||
}), | ||
}; | ||
} | ||
|
||
/** | ||
* Returns a reducer function configured according to the `reducerConfig` instance variable. | ||
*/ | ||
|
@@ -457,4 +686,20 @@ export default class ApolloClient { | |
queryDeduplication: this.queryDeduplication, | ||
}); | ||
}; | ||
|
||
/** | ||
* Initializes a data proxy for this client instance if one does not already | ||
* exist and returns either a previously initialized proxy instance or the | ||
* newly initialized instance. | ||
*/ | ||
private initProxy(): DataProxy { | ||
if (!this.proxy) { | ||
this.initStore(); | ||
this.proxy = new ReduxDataProxy( | ||
this.store, | ||
this.reduxRootSelector || defaultReduxRootSelector, | ||
); | ||
} | ||
return this.proxy; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to rebase the changelog