From e3e8b663e07ea5b3bcbbfdf452afb1dc9e8d44de Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 30 Jan 2020 15:38:17 -0500 Subject: [PATCH 1/3] Eliminate cache.writeData, a hack worse than the alternatives. I referred to cache.writeData as a "foot-seeking missile" in PR #5909, because it's one of the easiest ways to turn your faulty assumptions about how the cache represents data internally into cache corruption. PR #5909 introduced an alternative api, cache.modify(id, modifiers), which aims to take the place of the more "surgical" uses of cache.writeData. However, as you can see, in almost every case where cache.writeData was used in our tests, an appropriate query was already sitting very close by, making cache.writeQuery just as easy to call. If you think your life is worse now that you have to pass a query to cache.writeQuery or a fragment to cache.writeFragment, please realize that cache.writeData was dynamically creating a fresh query or fragment behind the scenes, every time it was called, so it was actually doing a lot more work than the equivalent call to cache.writeQuery or cache.writeFragment. --- src/ApolloClient.ts | 18 -- src/__tests__/ApolloClient.ts | 165 ------------------ src/__tests__/local-state/export.ts | 48 +++-- src/__tests__/local-state/general.ts | 16 +- src/__tests__/local-state/resolvers.ts | 129 +++----------- .../__tests__/__snapshots__/utils.ts.snap | 63 ------- src/cache/core/__tests__/cache.ts | 48 ----- src/cache/core/__tests__/utils.ts | 77 -------- src/cache/core/cache.ts | 56 ------ src/cache/core/types/Cache.ts | 1 - src/cache/core/types/DataProxy.ts | 18 -- src/cache/core/utils.ts | 65 ------- src/core/__tests__/ObservableQuery.ts | 34 +--- 13 files changed, 83 insertions(+), 655 deletions(-) delete mode 100644 src/cache/core/__tests__/__snapshots__/utils.ts.snap delete mode 100644 src/cache/core/__tests__/utils.ts delete mode 100644 src/cache/core/utils.ts diff --git a/src/ApolloClient.ts b/src/ApolloClient.ts index 8b9e15c20ff..32d1f087124 100644 --- a/src/ApolloClient.ts +++ b/src/ApolloClient.ts @@ -427,24 +427,6 @@ export class ApolloClient implements DataProxy { return result; } - /** - * Sugar for writeQuery & writeFragment - * This method will construct a query from the data object passed in. - * If no id is supplied, writeData will write the data to the root. - * If an id is supplied, writeData will write a fragment to the object - * specified by the id in the store. - * - * Since you aren't passing in a query to check the shape of the data, - * you must pass in an object that conforms to the shape of valid GraphQL data. - */ - public writeData( - options: DataProxy.WriteDataOptions, - ): void { - const result = this.cache.writeData(options); - this.queryManager.broadcastQueries(); - return result; - } - public __actionHookForDevTools(cb: () => any) { this.devToolsHookCb = cb; } diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 2463232465b..b854ee46c8b 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -1422,171 +1422,6 @@ describe('ApolloClient', () => { }); }); - describe('writeData', () => { - it('lets you write to the cache by passing in data', () => { - const query = gql` - { - field - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - }); - - client.writeData({ data: { field: 1 } }); - - return client.query({ query }).then(({ data }) => { - expect(stripSymbols({ ...data })).toEqual({ field: 1 }); - }); - }); - - it('lets you write to an existing object in the cache using an ID', () => { - const query = gql` - { - obj { - field - } - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - }); - - client.writeQuery({ - query, - data: { - obj: { field: 1, id: 'uniqueId', __typename: 'Object' }, - }, - }); - - client.writeData({ id: 'Object:uniqueId', data: { field: 2 } }); - - return client.query({ query }).then(({ data }: any) => { - expect(data.obj.field).toEqual(2); - }); - }); - - it(`doesn't overwrite __typename when writing to the cache with an id`, () => { - const query = gql` - { - obj { - field { - field2 - } - id - } - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - }); - - client.writeQuery({ - query, - data: { - obj: { - field: { field2: 1, __typename: 'Field' }, - id: 'uniqueId', - __typename: 'Object', - }, - }, - }); - - client.writeData({ - id: 'Object:uniqueId', - data: { field: { field2: 2, __typename: 'Field' } }, - }); - - return client - .query({ query }) - .then(({ data }: any) => { - expect(data.obj.__typename).toEqual('Object'); - expect(data.obj.field.__typename).toEqual('Field'); - }) - .catch(e => console.log(e)); - }); - - it(`adds a __typename for an object without one when writing to the cache with an id`, () => { - const query = gql` - { - obj { - field { - field2 - } - id - } - } - `; - - // This would cause a warning to be printed because we don't have - // __typename on the obj field. But that's intentional because - // that's exactly the situation we're trying to test... - - // Let's swap out console.warn to suppress this one message - - const suppressString = '__typename'; - const originalWarn = console.warn; - console.warn = (...args: any[]) => { - if ( - args.find(element => { - if (typeof element === 'string') { - return element.indexOf(suppressString) !== -1; - } - return false; - }) != null - ) { - // Found a thing in the args we told it to exclude - return; - } - originalWarn.apply(console, args); - }; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - }); - - client.writeQuery({ - query, - data: { - obj: { - __typename: 'Obj', - field: { - field2: 1, - __typename: 'Field', - }, - id: 'uniqueId', - }, - }, - }); - - client.writeData({ - id: 'Obj:uniqueId', - data: { - field: { - field2: 2, - __typename: 'Field', - }, - }, - }); - - return client - .query({ query }) - .then(({ data }: any) => { - console.warn = originalWarn; - expect(data.obj.__typename).toEqual('Obj'); - expect(data.obj.field.__typename).toEqual('Field'); - }) - .catch(e => console.log(e)); - }); - }); - describe('write then read', () => { it('will write data locally which will then be read back', () => { const client = new ApolloClient({ diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index 0fb7262c44d..a3ce48dc761 100644 --- a/src/__tests__/local-state/export.ts +++ b/src/__tests__/local-state/export.ts @@ -24,7 +24,11 @@ describe('@client @export tests', () => { link: ApolloLink.empty(), resolvers: {}, }); - cache.writeData({ data: { field: 1 } }); + + cache.writeQuery({ + query, + data: { field: 1 }, + }); return client.query({ query }).then(({ data }: any) => { expect({ ...data }).toMatchObject({ field: 1 }); @@ -54,7 +58,8 @@ describe('@client @export tests', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { car: { engine: { @@ -106,7 +111,8 @@ describe('@client @export tests', () => { }, }); - cache.writeData({ + cache.writeQuery({ + query, data: { currentAuthorId: testAuthorId, }, @@ -156,7 +162,8 @@ describe('@client @export tests', () => { }, }); - cache.writeData({ + cache.writeQuery({ + query, data: { currentAuthor: testAuthor, }, @@ -206,7 +213,8 @@ describe('@client @export tests', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { currentAuthor: testAuthor, }, @@ -268,7 +276,8 @@ describe('@client @export tests', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { appContainer, }, @@ -371,7 +380,8 @@ describe('@client @export tests', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { postRequiringReview: { loggedInReviewerId, @@ -450,7 +460,8 @@ describe('@client @export tests', () => { }, }); - cache.writeData({ + cache.writeQuery({ + query, data: { postRequiringReview: { __typename: 'Post', @@ -560,7 +571,8 @@ describe('@client @export tests', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query: gql`{ topPost }`, data: { topPost: testPostId, }, @@ -685,7 +697,8 @@ describe('@client @export tests', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { primaryReviewerId, secondaryReviewerId, @@ -736,7 +749,10 @@ describe('@client @export tests', () => { resolvers: {}, }); - client.writeData({ data: { currentAuthorId: testAuthorId1 } }); + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); const obs = client.watchQuery({ query }); obs.subscribe({ @@ -746,7 +762,10 @@ describe('@client @export tests', () => { currentAuthorId: testAuthorId1, postCount: testPostCount1, }); - client.writeData({ data: { currentAuthorId: testAuthorId2 } }); + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId2 }, + }); } else if (resultCount === 1) { expect({ ...data }).toMatchObject({ currentAuthorId: testAuthorId2, @@ -796,7 +815,10 @@ describe('@client @export tests', () => { resolvers: {}, }); - client.writeData({ data: { currentAuthorId: testAuthorId1 } }); + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); const obs = client.watchQuery({ query }); obs.subscribe({ diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts index f5e5d9f07d7..039b1444980 100644 --- a/src/__tests__/local-state/general.ts +++ b/src/__tests__/local-state/general.ts @@ -816,7 +816,8 @@ describe('Combining client and server state/operations', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { count: 0, }, @@ -848,7 +849,7 @@ describe('Combining client and server state/operations', () => { user: { __typename: 'User', // We need an id (or a keyFields policy) because, if the User - // object is not identifiable, the call to cache.writeData + // object is not identifiable, the call to cache.writeQuery // below will simply replace the existing data rather than // merging the new data with the existing data. id: 123, @@ -864,7 +865,8 @@ describe('Combining client and server state/operations', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { user: { __typename: 'User', @@ -950,14 +952,18 @@ describe('Combining client and server state/operations', () => { incrementCount: (_, __, { cache }) => { const { count } = cache.readQuery({ query: counterQuery }); const data = { count: count + 1 }; - cache.writeData({ data }); + cache.writeQuery({ + query: counterQuery, + data, + }); return null; }, }, }, }); - cache.writeData({ + cache.writeQuery({ + query: counterQuery, data: { count: 0, }, diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts index f1c5f819f96..df1c2aeeb95 100644 --- a/src/__tests__/local-state/resolvers.ts +++ b/src/__tests__/local-state/resolvers.ts @@ -508,7 +508,7 @@ describe('Writing cache data from resolvers', () => { resolvers: { Mutation: { start(_data, _args, { cache }) { - cache.writeData({ data: { field: 1 } }); + cache.writeQuery({ query, data: { field: 1 } }); return { start: true }; }, }, @@ -538,19 +538,28 @@ describe('Writing cache data from resolvers', () => { } `; + const cache = new InMemoryCache(); + const client = new ApolloClient({ - cache: new InMemoryCache(), + cache, link: ApolloLink.empty(), resolvers: { Mutation: { - start(_data, _args, { cache }) { + start() { cache.writeQuery({ query, data: { obj: { field: 1, id: 'uniqueId', __typename: 'Object' }, }, }); - cache.writeData({ id: 'Object:uniqueId', data: { field: 2 } }); + + cache.modify('Object:uniqueId', { + field(value) { + expect(value).toBe(1); + return 2; + }, + }) + return { start: true }; }, }, @@ -583,12 +592,14 @@ describe('Writing cache data from resolvers', () => { } `; + const cache = new InMemoryCache(); + const client = new ApolloClient({ - cache: new InMemoryCache(), + cache, link: ApolloLink.empty(), resolvers: { Mutation: { - start(_data, _args, { cache }) { + start() { cache.writeQuery({ query, data: { @@ -599,9 +610,11 @@ describe('Writing cache data from resolvers', () => { }, }, }); - cache.writeData({ - id: 'Object:uniqueId', - data: { field: { field2: 2, __typename: 'Field' } }, + cache.modify('Object:uniqueId', { + field(value) { + expect(value.field2).toBe(1); + return { ...value, field2: 2 }; + }, }); return { start: true }; }, @@ -618,95 +631,6 @@ describe('Writing cache data from resolvers', () => { }) .catch(e => console.log(e)); }); - - it( - 'should add a __typename for an object without one when writing to the ' + - 'cache with an id', - () => { - const query = gql` - { - obj @client { - field { - field2 - } - id - } - } - `; - - const mutation = gql` - mutation start { - start @client - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers: { - Mutation: { - start(_data, _args, { cache }) { - // This would cause a warning to be printed because we don't have - // __typename on the obj field. But that's intentional because - // that's exactly the situation we're trying to test... - - // Let's swap out console.warn to suppress this one message - const suppressString = '__typename'; - const originalWarn = console.warn; - console.warn = (...args: any[]) => { - if ( - args.find(element => { - if (typeof element === 'string') { - return element.indexOf(suppressString) !== -1; - } - return false; - }) != null - ) { - // Found a thing in the args we told it to exclude - return; - } - originalWarn.apply(console, args); - }; - // Actually call the problematic query - cache.writeQuery({ - query, - data: { - obj: { - field: { field2: 1, __typename: 'Field' }, - id: 'uniqueId', - }, - }, - }); - // Restore warning logger - console.warn = originalWarn; - - cache.writeData({ - id: 'ROOT_QUERY', - data: { - obj: { - field: { - field2: 2, - __typename: 'Field', - }, - }, - }, - }); - return { start: true }; - }, - }, - }, - }); - - return client - .mutate({ mutation }) - .then(() => client.query({ query })) - .then(({ data }: any) => { - expect(data.obj.__typename).toEqual('__ClientData'); - expect(data.obj.field.__typename).toEqual('Field'); - }) - .catch(e => console.log(e)); - }, - ); }); describe('Resolving field aliases', () => { @@ -848,7 +772,8 @@ describe('Resolving field aliases', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query: gql`{ foo { bar }}`, data: { foo: { bar: 'yo', @@ -905,7 +830,8 @@ describe('Resolving field aliases', () => { }, }); - client.writeData({ + client.writeQuery({ + query: gql`{ launch { isInCart }}`, data: { launch: { isInCart: false, @@ -950,7 +876,8 @@ describe('Force local resolvers', () => { resolvers: {}, }); - cache.writeData({ + cache.writeQuery({ + query, data: { author: { name: 'John Smith', diff --git a/src/cache/core/__tests__/__snapshots__/utils.ts.snap b/src/cache/core/__tests__/__snapshots__/utils.ts.snap deleted file mode 100644 index 2a167ec7a18..00000000000 --- a/src/cache/core/__tests__/__snapshots__/utils.ts.snap +++ /dev/null @@ -1,63 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[ - `writing data with no query converts a JavaScript object to a query correctly arrays 1` -] = ` -"query GeneratedClientQuery { - number - bool - nested { - bool2 - undef - nullField - str - } -} -" -`; - -exports[ - `writing data with no query converts a JavaScript object to a query correctly basic 1` -] = ` -"query GeneratedClientQuery { - number - bool - bool2 - undef - nullField - str -} -" -`; - -exports[ - `writing data with no query converts a JavaScript object to a query correctly fragments 1` -] = ` -"fragment GeneratedClientQuery on __FakeType { - number - bool - nested { - bool2 - undef - nullField - str - } -} -" -`; - -exports[ - `writing data with no query converts a JavaScript object to a query correctly nested 1` -] = ` -"query GeneratedClientQuery { - number - bool - nested { - bool2 - undef - nullField - str - } -} -" -`; diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts index 6f10d7c57d8..d85f3aa7c1d 100644 --- a/src/cache/core/__tests__/cache.ts +++ b/src/cache/core/__tests__/cache.ts @@ -98,52 +98,4 @@ describe('abstract cache', () => { expect(test.write).toBeCalled(); }); }); - - describe('writeData', () => { - it('either writes a fragment or a query', () => { - const test = new TestCache(); - test.read = jest.fn(); - test.writeFragment = jest.fn(); - test.writeQuery = jest.fn(); - - test.writeData({}); - expect(test.writeQuery).toBeCalled(); - - test.writeData({ id: 1 }); - expect(test.read).toBeCalled(); - expect(test.writeFragment).toBeCalled(); - - // Edge case for falsey id - test.writeData({ id: 0 }); - expect(test.read).toHaveBeenCalledTimes(2); - expect(test.writeFragment).toHaveBeenCalledTimes(2); - }); - - it('suppresses read errors', () => { - const test = new TestCache(); - test.read = () => { - throw new Error(); - }; - test.writeFragment = jest.fn(); - - expect(() => test.writeData({ id: 1 })).not.toThrow(); - expect(test.writeFragment).toBeCalled(); - }); - - it('reads __typename from typenameResult or defaults to __ClientData', () => { - const test = new TestCache(); - test.read = () => ({ __typename: 'a' }); - let res; - test.writeFragment = obj => - (res = obj.fragment.definitions[0].typeCondition.name.value); - - test.writeData({ id: 1 }); - expect(res).toBe('a'); - - test.read = () => ({}); - - test.writeData({ id: 1 }); - expect(res).toBe('__ClientData'); - }); - }); }); diff --git a/src/cache/core/__tests__/utils.ts b/src/cache/core/__tests__/utils.ts deleted file mode 100644 index 213d67e728b..00000000000 --- a/src/cache/core/__tests__/utils.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { print } from 'graphql/language/printer'; - -import { queryFromPojo, fragmentFromPojo } from '../utils'; - -describe('writing data with no query', () => { - describe('converts a JavaScript object to a query correctly', () => { - it('basic', () => { - expect( - print( - queryFromPojo({ - number: 5, - bool: true, - bool2: false, - undef: undefined, - nullField: null, - str: 'string', - }), - ), - ).toMatchSnapshot(); - }); - - it('nested', () => { - expect( - print( - queryFromPojo({ - number: 5, - bool: true, - nested: { - bool2: false, - undef: undefined, - nullField: null, - str: 'string', - }, - }), - ), - ).toMatchSnapshot(); - }); - - it('arrays', () => { - expect( - print( - queryFromPojo({ - number: [5], - bool: [[true]], - nested: [ - { - bool2: false, - undef: undefined, - nullField: null, - str: 'string', - }, - ], - }), - ), - ).toMatchSnapshot(); - }); - - it('fragments', () => { - expect( - print( - fragmentFromPojo({ - number: [5], - bool: [[true]], - nested: [ - { - bool2: false, - undef: undefined, - nullField: null, - str: 'string', - }, - ], - }), - ), - ).toMatchSnapshot(); - }); - }); -}); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index 255f1e6cb9b..cb92a5423d5 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -5,27 +5,8 @@ import { getFragmentQueryDocument } from '../../utilities/graphql/fragments'; import { StoreObject } from '../../utilities/graphql/storeUtils'; import { DataProxy } from './types/DataProxy'; import { Cache } from './types/Cache'; -import { queryFromPojo, fragmentFromPojo } from './utils'; import { Modifier, Modifiers } from './types/common'; -const justTypenameQuery: DocumentNode = { - kind: "Document", - definitions: [{ - kind: "OperationDefinition", - operation: "query", - selectionSet: { - kind: "SelectionSet", - selections: [{ - kind: "Field", - name: { - kind: "Name", - value: "__typename", - }, - }], - }, - }], -}; - export type Transaction = (c: ApolloCache) => void; export abstract class ApolloCache implements DataProxy { @@ -161,41 +142,4 @@ export abstract class ApolloCache implements DataProxy { query: this.getFragmentDoc(options.fragment, options.fragmentName), }); } - - public writeData({ - id, - data, - }: Cache.WriteDataOptions): void { - if (typeof id !== 'undefined') { - let typenameResult = null; - // Since we can't use fragments without having a typename in the store, - // we need to make sure we have one. - // To avoid overwriting an existing typename, we need to read it out first - // and generate a fake one if none exists. - try { - typenameResult = this.read({ - rootId: id, - optimistic: false, - query: justTypenameQuery, - }); - } catch (e) { - // Do nothing, since an error just means no typename exists - } - - // tslint:disable-next-line - const __typename = - (typenameResult && typenameResult.__typename) || '__ClientData'; - - // Add a type here to satisfy the inmemory cache - const dataToWrite = Object.assign({ __typename }, data); - - this.writeFragment({ - id, - fragment: fragmentFromPojo(dataToWrite, __typename), - data: dataToWrite, - }); - } else { - this.writeQuery({ query: queryFromPojo(data), data }); - } - } } diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 596a4cd5ceb..41bab402bab 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -28,6 +28,5 @@ export namespace Cache { export import DiffResult = DataProxy.DiffResult; export import WriteQueryOptions = DataProxy.WriteQueryOptions; export import WriteFragmentOptions = DataProxy.WriteFragmentOptions; - export import WriteDataOptions = DataProxy.WriteDataOptions; export import Fragment = DataProxy.Fragment; } diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts index 27af0ffff09..e56952d014d 100644 --- a/src/cache/core/types/DataProxy.ts +++ b/src/cache/core/types/DataProxy.ts @@ -67,16 +67,6 @@ export namespace DataProxy { data: TData; } - export interface WriteDataOptions { - /** - * The data you will be writing to the store. - * It also takes an optional id property. - * The id is used to write a fragment to an existing object in the store. - */ - data: TData; - id?: string; - } - export type DiffResult = { result?: T; complete?: boolean; @@ -123,12 +113,4 @@ export interface DataProxy { writeFragment( options: DataProxy.WriteFragmentOptions, ): void; - - /** - * Sugar for writeQuery & writeFragment. - * Writes data to the store without passing in a query. - * If you supply an id, the data will be written as a fragment to an existing object. - * Otherwise, the data is written to the root of the store. - */ - writeData(options: DataProxy.WriteDataOptions): void; } diff --git a/src/cache/core/utils.ts b/src/cache/core/utils.ts deleted file mode 100644 index 7b48d175714..00000000000 --- a/src/cache/core/utils.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - DocumentNode, - SelectionSetNode, -} from 'graphql'; - -export function queryFromPojo(obj: any): DocumentNode { - return { - kind: 'Document', - definitions: [{ - kind: 'OperationDefinition', - operation: 'query', - name: { - kind: 'Name', - value: 'GeneratedClientQuery', - }, - selectionSet: selectionSetFromObj(obj), - }], - }; -} - -export function fragmentFromPojo(obj: any, typename?: string): DocumentNode { - return { - kind: 'Document', - definitions: [{ - kind: 'FragmentDefinition', - typeCondition: { - kind: 'NamedType', - name: { - kind: 'Name', - value: typename || '__FakeType', - }, - }, - name: { - kind: 'Name', - value: 'GeneratedClientQuery', - }, - selectionSet: selectionSetFromObj(obj), - }], - }; -} - -function selectionSetFromObj(obj: any): SelectionSetNode { - if (!obj || Object(obj) !== obj) { - // No selection set here - return null; - } - - if (Array.isArray(obj)) { - // GraphQL queries don't include arrays - return selectionSetFromObj(obj[0]); - } - - // Now we know it's an object - return { - kind: 'SelectionSet', - selections: Object.keys(obj).map(key => ({ - kind: 'Field', - name: { - kind: 'Name', - value: key, - }, - selectionSet: selectionSetFromObj(obj[key]) || void 0, - })), - }; -} diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 5c8e3dbe9e1..b69445d9f66 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -1872,6 +1872,7 @@ describe('ObservableQuery', () => { assumeImmutableResults = true, assertFrozenResults = false, }) { + const cache = new InMemoryCache(); const client = new ApolloClient({ link: mockSingleLink( { request: queryOptions, result: { data: { value: 1 } } }, @@ -1879,7 +1880,7 @@ describe('ObservableQuery', () => { { request: queryOptions, result: { data: { value: 3 } } } ).setOnError(error => { throw error }), assumeImmutableResults, - cache: new InMemoryCache(), + cache, }); const observable = client.watchQuery(queryOptions); @@ -1887,24 +1888,21 @@ describe('ObservableQuery', () => { return new Promise((resolve, reject) => { observable.subscribe({ - next(result) { - values.push(result.data.value); + next({ data }) { + values.push(data.value); if (assertFrozenResults) { try { - result.data.value = 'oyez'; + data.value = 'oyez'; } catch (error) { reject(error); } } else { - result = { - ...result, - data: { - ...result.data, - value: 'oyez', - }, + data = { + ...data, + value: 'oyez', }; } - client.writeData(result); + client.writeQuery({ query, data }); }, error(err) { expect(err.message).toMatch(/No more mocked responses/); @@ -1914,20 +1912,6 @@ describe('ObservableQuery', () => { }); } - // When we do not assume immutable results, the observable must do - // extra work to take snapshots of past results, just in case those - // results are destructively modified. The benefit of that work is - // that such mutations can be detected, which is why "oyez" appears - // in the list of values here. This is a somewhat indirect way of - // detecting that cloneDeep must have been called, but at least it - // doesn't violate any abstractions. - expect( - await check({ - assumeImmutableResults: false, - assertFrozenResults: false, - }), - ).toEqual([1, 'oyez', 2, 'oyez', 3, 'oyez']); - async function checkThrows(assumeImmutableResults) { try { await check({ From 36559aa2c63a658c3200f317f75b9b7e3b54ec67 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 25 Feb 2020 12:07:12 -0500 Subject: [PATCH 2/3] Remove cache.writeData from local state documentation. --- CHANGELOG.md | 3 + docs/shared/mutation-result.mdx | 2 +- docs/source/data/local-state.mdx | 212 +++++++++++++++++++------------ 3 files changed, 134 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d3a652992..243e699939f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,9 @@ - **[BREAKING]** Apollo Client 2.x allowed `@client` fields to be passed into the `link` chain if `resolvers` were not set in the constructor. This allowed `@client` fields to be passed into Links like `apollo-link-state`. Apollo Client 3 enforces that `@client` fields are local only, meaning they are no longer passed into the `link` chain, under any circumstances.
[@hwillson](https://github.com/hwillson) in [#5982](https://github.com/apollographql/apollo-client/pull/5982) +- **[BREAKING]** `client|cache.writeData` have been fully removed. `writeData` usage is one of the easiest ways to turn faulty assumptions about how the cache represents data internally, into cache inconsistency and corruption. `client|cache.writeQuery`, `client|cache.writeFragment`, and/or `cache.modify` can be used to update the cache.
+ [@benjamn](https://github.com/benjamn) in [#5923](https://github.com/apollographql/apollo-client/pull/5923) + - `InMemoryCache` now supports tracing garbage collection and eviction. Note that the signature of the `evict` method has been simplified in a potentially backwards-incompatible way.
[@benjamn](https://github.com/benjamn) in [#5310](https://github.com/apollographql/apollo-client/pull/5310) diff --git a/docs/shared/mutation-result.mdx b/docs/shared/mutation-result.mdx index 93208fa1d69..6017aad9953 100644 --- a/docs/shared/mutation-result.mdx +++ b/docs/shared/mutation-result.mdx @@ -12,4 +12,4 @@ | `loading` | boolean | A boolean indicating whether your mutation is in flight | | `error` | ApolloError | Any errors returned from the mutation | | `called` | boolean | A boolean indicating if the mutate function has been called | -| `client` | ApolloClient | Your `ApolloClient` instance. Useful for invoking cache methods outside the context of the update function, such as `client.writeData` and `client.readQuery`. | +| `client` | ApolloClient | Your `ApolloClient` instance. Useful for invoking cache methods outside the context of the update function, such as `client.writeQuery` and `client.readQuery`. | diff --git a/docs/source/data/local-state.mdx b/docs/source/data/local-state.mdx index d5b6107c4a5..eb80f5c3cef 100644 --- a/docs/source/data/local-state.mdx +++ b/docs/source/data/local-state.mdx @@ -17,7 +17,7 @@ Please note that this documentation is intended to be used to familiarize yourse ## Updating local state -There are two main ways to perform local state mutations. The first way is to directly write to the cache by calling `cache.writeData`. Direct writes are great for one-off mutations that don't depend on the data that's currently in the cache, such as writing a single value. The second way is by leveraging the `useMutation` hook with a GraphQL mutation that calls a local client-side resolver. We recommend using resolvers if your mutation depends on existing values in the cache, such as adding an item to a list or toggling a boolean. +There are two main ways to perform local state mutations. The first way is to directly write to the cache by calling `cache.writeQuery`. Direct writes are great for one-off mutations that don't depend on the data that's currently in the cache, such as writing a single value. The second way is by leveraging the `useMutation` hook with a GraphQL mutation that calls a local client-side resolver. We recommend using resolvers if your mutation depends on existing values in the cache, such as adding an item to a list or toggling a boolean. ### Direct writes @@ -36,7 +36,10 @@ function FilterLink({ filter, children }) { const client = useApolloClient(); return ( client.writeData({ data: { visibilityFilter: filter } })} + onClick={() => client.writeQuery({ + query: gql`{ visibilityFilter }`, + data: { visibilityFilter: filter }, + })} > {children} @@ -57,7 +60,10 @@ const FilterLink = ({ filter, children }) => ( {client => ( client.writeData({ data: { visibilityFilter: filter } })} + onClick={() => client.writeQuery({ + query: gql`{ visibilityFilter }`, + data: { visibilityFilter: filter }, + })} > {children} @@ -69,7 +75,7 @@ const FilterLink = ({ filter, children }) => ( -The `ApolloConsumer` render prop function is called with a single value, the Apollo Client instance. You can think of the `ApolloConsumer` component as being similar to the `Consumer` component from the [React context API](https://reactjs.org/docs/context.html). From the client instance, you can directly call `client.writeData` and pass in the data you'd like to write to the cache. +The `ApolloConsumer` render prop function is called with a single value, the Apollo Client instance. You can think of the `ApolloConsumer` component as being similar to the `Consumer` component from the [React context API](https://reactjs.org/docs/context.html). From the client instance, you can directly call `client.writeQuery` and pass in the data you'd like to write to the cache. What if we want to immediately subscribe to the data we just wrote to the cache? Let's create an `active` property on the link that marks the link's filter as active if it's the same as the current `visibilityFilter` in the cache. To immediately subscribe to a client-side mutation, we can use `useQuery`. The `useQuery` hook also makes the client instance available in its result object. @@ -92,7 +98,10 @@ function FilterLink({ filter, children }) { const { data, client } = useQuery(GET_VISIBILITY_FILTER); return ( client.writeData({ data: { visibilityFilter: filter } })} + onClick={() => client.writeQuery({ + query: GET_VISIBILITY_FILTER, + data: { visibilityFilter: filter }, + })} active={data.visibilityFilter === filter} > {children} @@ -121,7 +130,10 @@ const FilterLink = ({ filter, children }) => ( {({ data, client }) => ( client.writeData({ data: { visibilityFilter: filter } })} + onClick={() => client.writeQuery({ + query: GET_VISIBILITY_FILTER, + data: { visibilityFilter: filter }, + })} active={data.visibilityFilter === filter} > {children} @@ -134,7 +146,7 @@ const FilterLink = ({ filter, children }) => ( -You'll notice in our query that we have a `@client` directive next to our `visibilityFilter` field. This tells Apollo Client to fetch the field data locally (either from the cache or using a local resolver), instead of sending it to our GraphQL server. Once you call `client.writeData`, the query result on the render prop function will automatically update. All cache writes and reads are synchronous, so you don't have to worry about loading state. +You'll notice in our query that we have a `@client` directive next to our `visibilityFilter` field. This tells Apollo Client to fetch the field data locally (either from the cache or using a local resolver), instead of sending it to our GraphQL server. Once you call `client.writeQuery`, the query result on the render prop function will automatically update. All cache writes and reads are synchronous, so you don't have to worry about loading state. ### Local resolvers @@ -150,7 +162,7 @@ fieldName: (obj, args, context, info) => result; 2. `args`: An object containing all of the arguments passed into the field. For example, if you called a mutation with `updateNetworkStatus(isConnected: true)`, the `args` object would be `{ isConnected: true }`. 3. `context`: An object of contextual information shared between your React components and your Apollo Client network stack. In addition to any custom context properties that may be present, local resolvers always receive the following: - `context.client`: The Apollo Client instance. - - `context.cache`: The Apollo Cache instance, which can be used to manipulate the cache with `context.cache.readQuery`, `.writeQuery`, `.readFragment`, `.writeFragment`, and `.writeData`. You can learn more about these methods in [Managing the cache](#managing-the-cache). + - `context.cache`: The Apollo Cache instance, which can be used to manipulate the cache with `context.cache.readQuery`, `.writeQuery`, `.readFragment`, `.writeFragment`, `.modify`, and `.evict`. You can learn more about these methods in [Managing the cache](#managing-the-cache). - `context.getCacheKey`: Get a key from the cache using a `__typename` and `id`. 4. `info`: Information about the execution state of the query. You will probably never have to use this one. @@ -163,26 +175,25 @@ const client = new ApolloClient({ cache: new InMemoryCache(), resolvers: { Mutation: { - toggleTodo: (_root, variables, { cache, getCacheKey }) => { - const id = getCacheKey({ __typename: 'TodoItem', id: variables.id }) - const fragment = gql` - fragment completeTodo on TodoItem { - completed - } - `; - const todo = cache.readFragment({ fragment, id }); - const data = { ...todo, completed: !todo.completed }; - cache.writeData({ id, data }); - return null; + toggleTodo: (_root, variables, { cache }) => { + const id = cache.identify({ + __typename: 'TodoItem', + id: variables.id, + }); + cache.modify(id, { + completed(value) { + return !value; + }, + }); }, }, }, }); ``` -In order to toggle the todo's completed status, we first need to query the cache to find out what the todo's current completed status is. We do this by reading a fragment from the cache with `cache.readFragment`. This function takes a fragment and an id, which corresponds to the todo item's cache key. We get the cache key by calling the `getCacheKey` that's on the context and passing in the item's `__typename` and `id`. +In previous versions of Apollo Client, toggling the `completed` status of the `TodoItem` required reading a fragment from the cache, modifying the result by negating the `completed` boolean, and then writing the fragment back into the cache. Apollo Client 3.0 introduced the `cache.modify` method as an easier and faster way to update specific fields within a given entity object. To determine the ID of the entity, we pass the `__typename` and primary key fields of the object to `cache.identify` method. -Once we read the fragment, we toggle the todo's completed status and write the updated data back to the cache. Since we don't plan on using the mutation's return result in our UI, we return null since all GraphQL types are nullable by default. +Once we toggle the `completed` field, since we don't plan on using the mutation's return result in our UI, we return `null` since all GraphQL types are nullable by default. Let's learn how to trigger our `toggleTodo` mutation from our component: @@ -328,7 +339,7 @@ Here we create our GraphQL query and add `@client` directives to `todos` and `vi ### Initializing the cache -Often, you'll need to write an initial state to the cache so any components querying data before a mutation is triggered don't error out. To accomplish this, you can use `cache.writeData` to prep the cache with initial values. The shape of your initial state should match how you plan to query it in your application. +Often, you'll need to write an initial state to the cache so any components querying data before a mutation is triggered don't error out. To accomplish this, you can use `cache.writeQuery` to prep the cache with initial values. ```js import { ApolloClient, InMemoryCache } from '@apollo/client'; @@ -339,7 +350,16 @@ const client = new ApolloClient({ resolvers: { /* ... */ }, }); -cache.writeData({ +cache.writeQuery({ + query: gql` + query { + todos + visibilityFilter + networkStatus { + isConnected + } + } + `, data: { todos: [], visibilityFilter: 'SHOW_ALL', @@ -351,7 +371,7 @@ cache.writeData({ }); ``` -Sometimes you may need to [reset the store](../api/core/#ApolloClient.resetStore) in your application, when a user logs out for example. If you call `client.resetStore` anywhere in your application, you will likely want to initialize your cache again. You can do this using the `client.onResetStore` method to register a callback that will call `cache.writeData` again. +Sometimes you may need to [reset the store](../api/core/#ApolloClient.resetStore) in your application, when a user logs out for example. If you call `client.resetStore` anywhere in your application, you will likely want to initialize your cache again. You can do this using the `client.onResetStore` method to register a callback that will call `cache.writeQuery` again. ```js import { ApolloClient, InMemoryCache } from '@apollo/client'; @@ -362,18 +382,31 @@ const client = new ApolloClient({ resolvers: { /* ... */ }, }); -const data = { - todos: [], - visibilityFilter: 'SHOW_ALL', - networkStatus: { - __typename: 'NetworkStatus', - isConnected: false, - }, -}; +function writeInitialData() { + cache.writeQuery({ + query: gql` + query { + todos + visibilityFilter + networkStatus { + isConnected + } + } + `, + data: { + todos: [], + visibilityFilter: 'SHOW_ALL', + networkStatus: { + __typename: 'NetworkStatus', + isConnected: false, + }, + }, + }); +} -cache.writeData({ data }); +writeInitialData(); -client.onResetStore(() => cache.writeData({ data })); +client.onResetStore(writeInitialData); ``` ### Local data query flow @@ -418,7 +451,8 @@ const GET_CART_ITEMS = gql` `; const cache = new InMemoryCache(); -cache.writeData({ +cache.writeQuery({ + query: GET_CART_ITEMS, data: { cartItems: [], }, @@ -642,18 +676,19 @@ const client = new ApolloClient({ resolvers: {}, }); -cache.writeData({ - data: { - isLoggedIn: !!localStorage.getItem("token"), - }, -}); - const IS_LOGGED_IN = gql` query IsUserLoggedIn { isLoggedIn @client } `; +cache.writeQuery({ + query: IS_LOGGED_IN, + data: { + isLoggedIn: !!localStorage.getItem("token"), + }, +}); + function App() { const { data } = useQuery(IS_LOGGED_IN); return data.isLoggedIn ? : ; @@ -692,18 +727,19 @@ const client = new ApolloClient({ resolvers: {}, }); -cache.writeData({ - data: { - isLoggedIn: !!localStorage.getItem("token"), - }, -}); - const IS_LOGGED_IN = gql` query IsUserLoggedIn { isLoggedIn @client } `; +cache.writeQuery({ + query: IS_LOGGED_IN, + data: { + isLoggedIn: !!localStorage.getItem("token"), + }, +}); + ReactDOM.render( @@ -717,7 +753,7 @@ ReactDOM.render( -In the above example, we first prep the cache using `cache.writeData` to store a value for the `isLoggedIn` field. We then run the `IS_LOGGED_IN` query via an Apollo Client `useQuery` hook, which includes an `@client` directive. When Apollo Client executes the `IS_LOGGED_IN` query, it first looks for a local resolver that can be used to handle the `@client` field. When it can't find one, it falls back on trying to pull the specified field out of the cache. So in this case, the `data` value returned by the `useQuery` hook has a `isLoggedIn` property available, which includes the `isLoggedIn` result (`!!localStorage.getItem('token')`) pulled directly from the cache. +In the above example, we first prep the cache using `cache.writeQuery` to store a value for the `isLoggedIn` field. We then run the `IS_LOGGED_IN` query via an Apollo Client `useQuery` hook, which includes an `@client` directive. When Apollo Client executes the `IS_LOGGED_IN` query, it first looks for a local resolver that can be used to handle the `@client` field. When it can't find one, it falls back on trying to pull the specified field out of the cache. So in this case, the `data` value returned by the `useQuery` hook has a `isLoggedIn` property available, which includes the `isLoggedIn` result (`!!localStorage.getItem('token')`) pulled directly from the cache. > ⚠️ If you want to use Apollo Client's `@client` support to query the cache without using local resolvers, you must pass an empty object into the `ApolloClient` constructor `resolvers` option. Without this Apollo Client will not enable its integrated `@client` support, which means your `@client` based queries will be passed to the Apollo Client link chain. You can find more details about why this is necessary [here](https://github.com/apollographql/apollo-client/pull/4499). @@ -907,7 +943,8 @@ const client = new ApolloClient({ resolvers: {}, }); -cache.writeData({ +cache.writeQuery({ + query: gql`{ currentAuthorId }`, data: { currentAuthorId: 12345, }, @@ -938,7 +975,15 @@ const client = new ApolloClient({ resolvers: {}, }); -cache.writeData({ +cache.writeQuery({ + query: gql` + query { + currentAuthor { + name + authorId + } + } + `, data: { currentAuthor: { __typename: 'Author', @@ -975,7 +1020,8 @@ const client = new ApolloClient({ }, }); -cache.writeData({ +cache.writeQuery({ + query: gql`{ currentAuthorId }`, data: { currentAuthorId: 12345, }, @@ -1002,9 +1048,9 @@ So here the `currentAuthorId` is loaded from the cache, then passed into the `po When you're using Apollo Client to work with local state, your Apollo cache becomes the single source of truth for all of your local and remote data. The [Apollo cache API](../caching/cache-interaction/) has several methods that can assist you with updating and retrieving data. Let's walk through the most relevant methods, and explore some common use cases for each one. -### writeData +### cache.writeQuery -The easiest way to update the cache is with `cache.writeData`, which allows you to write data directly to the cache without passing in a query. Here's how you use it in your resolver map for a simple update: +The easiest way to update the cache is with `cache.writeQuery`. Here's how you use it in your resolver map for a simple update: ```js import { ApolloClient, InMemoryCache } from '@apollo/client'; @@ -1014,17 +1060,20 @@ const client = new ApolloClient({ resolvers: { Mutation: { updateVisibilityFilter: (_, { visibilityFilter }, { cache }) => { - const data = { visibilityFilter, __typename: 'Filter' }; - cache.writeData({ data }); + cache.writeQuery({ + query: gql`{ visibilityFilter }`, + data: { + __typename: 'Filter', + visibilityFilter, + }, + }); }, }, }, }; ``` -`cache.writeData` also allows you to pass in an optional `id` property to write a fragment to an existing object in the cache. This is useful if you want to add some client-side fields to an existing object in the cache. - -The `id` should correspond to the object's cache key. If you're using the `InMemoryCache` and not overriding the `dataIdFromObject` config property, your cache key should be `__typename:id`. +The `cache.writeFragment` method allows you to pass in an optional `id` property to write a fragment to an existing object in the cache. This is useful if you want to add some client-side fields to an existing object in the cache. ```js import { ApolloClient, InMemoryCache } from '@apollo/client'; @@ -1034,19 +1083,22 @@ const client = new ApolloClient({ resolvers: { Mutation: { updateUserEmail: (_, { id, email }, { cache }) => { - const data = { email }; - cache.writeData({ id: `User:${id}`, data }); + cache.writeFragment({ + id: cache.identify({ __typename: "User", id }), + fragment: gql`fragment UserEmail on User { email }`, + data: { email }, + }); }, }, }, }; ``` -`cache.writeData` should cover most of your needs; however, there are some cases where the data you're writing to the cache depends on the data that's already there. In that scenario, you should use `readQuery` or `readFragment`, which allows you to pass in a query or a fragment to read data from the cache. If you'd like to validate the shape of your data that you're writing to the cache, use `writeQuery` or `writeFragment`. We'll explain some of those use cases below. +The `cache.writeQuery` and `cache.writeFragment` methods should cover most of your needs; however, there are some cases where the data you're writing to the cache depends on the data that's already there. In that scenario, you should use `cache.modify(id, modifiers)` to update specific fields within the entity object identified by `id`. ### writeQuery and readQuery -Sometimes, the data you're writing to the cache depends on data that's already in the cache; for example, you're adding an item to a list or setting a property based on an existing property value. In that case, you should use `cache.readQuery` to pass in a query and read a value from the cache before you write any data. Let's look at an example where we add a todo to a list: +Sometimes, the data you're writing to the cache depends on data that's already in the cache; for example, you're adding an item to a list or setting a property based on an existing property value. In that case, you should use `cache.modify` to update specific existing fields. Let's look at an example where we add a todo to a list: ```js import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; @@ -1054,10 +1106,10 @@ import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; let nextTodoId = 0; const cache = new InMemoryCache(); -cache.writeData({ - data: { - todos: [], - }, + +cache.writeQuery({ + query: gql`{ todos }`, + data: { todos: [] }, }); const client = new ApolloClient({ @@ -1080,7 +1132,6 @@ const client = new ApolloClient({ todos: [...previous.todos, newTodo], }; - // you can also do cache.writeData({ data }) here if you prefer cache.writeQuery({ query, data }); return newTodo; }, @@ -1089,9 +1140,7 @@ const client = new ApolloClient({ }); ``` -In order to add our todo to the list, we need the todos that are currently in the cache, which is why we call `cache.readQuery` to retrieve them. `cache.readQuery` will throw an error if the data isn't in the cache, so we need to provide an initial state. This is why we're calling `cache.writeData` with the empty array of todos after creating the `InMemoryCache`. - -To write the data to the cache, you can use either `cache.writeQuery` or `cache.writeData`. The only difference between the two is that `cache.writeQuery` requires that you pass in a query to validate that the shape of the data you're writing to the cache is the same as the shape of the data required by the query. Under the hood, `cache.writeData` automatically constructs a query from the `data` object you pass in and calls `cache.writeQuery`. +In order to add our todo to the list, we need the todos that are currently in the cache, which is why we call `cache.readQuery` to retrieve them. `cache.readQuery` will throw an error if the data isn't in the cache, so we need to provide an initial state. This is why we're calling `cache.writeQuery` with the empty array of todos after creating the `InMemoryCache`. ### writeFragment and readFragment @@ -1115,7 +1164,6 @@ const client = new ApolloClient({ const todo = cache.readFragment({ fragment, id }); const data = { ...todo, completed: !todo.completed }; - // you can also do cache.writeData({ data, id }) here if you prefer cache.writeFragment({ fragment, id, data }); return null; }, @@ -1126,8 +1174,6 @@ const client = new ApolloClient({ In order to toggle our todo, we need the todo and its status from the cache, which is why we call `cache.readFragment` and pass in a fragment to retrieve it. The `id` we're passing into `cache.readFragment` refers to its cache key. If you're using the `InMemoryCache` and not overriding the `dataIdFromObject` config property, your cache key should be `__typename:id`. -To write the data to the cache, you can use either `cache.writeFragment` or `cache.writeData`. The only difference between the two is that `cache.writeFragment` requires that you pass in a fragment to validate that the shape of the data you're writing to the cache node is the same as the shape of the data required by the fragment. Under the hood, `cache.writeData` automatically constructs a fragment from the `data` object and `id` you pass in and calls `cache.writeFragment`. - ## Client-side schema You can optionally set a client-side schema to be used with Apollo Client, through either the `ApolloClient` constructor `typeDefs` parameter, or the local state API `setTypeDefs` method. Your schema should be written in [Schema Definition Language](https://www.apollographql.com/docs/graphql-tools/generate-schema#schema-language). This schema is not used for validation like it is on the server because the `graphql-js` modules for schema validation would dramatically increase your bundle size. Instead, your client-side schema is used for introspection in [Apollo Client Devtools](https://github.com/apollographql/apollo-client-devtools), where you can explore your schema in GraphiQL. @@ -1315,7 +1361,7 @@ Updating your application to use Apollo Client's local state management features }); ``` -3. `defaults` are no longer supported. To prep the cache, use [`cache.writeData`](#writedata) directly instead. So +3. `defaults` are no longer supported. To prep the cache, use [`cache.writeQuery`](#writequery) directly instead. So ```js const cache = new InMemoryCache(); @@ -1341,10 +1387,9 @@ Updating your application to use Apollo Client's local state management features link: new HttpLink({ uri: '...' }), resolvers: { ... }, }); - cache.writeData({ - data: { - someField: 'some value', - }, + cache.writeQuery({ + query: gql`{ someField }`, + data: { someField: 'some value' }, }); ``` @@ -1432,7 +1477,11 @@ type FragmentMatcher = ( import { InMemoryCache } from '@apollo/client'; const cache = new InMemoryCache(); -cache.writeData({ +cache.writeQuery({ + query: gql`{ + isLoggedIn, + cartItems + }`, data: { isLoggedIn: !!localStorage.getItem('token'), cartItems: [], @@ -1442,8 +1491,7 @@ cache.writeData({ | Method | Description | | - | - | -| `writeData({ id, data })` | Write data directly to the root of the cache without having to pass in a query. Great for prepping the cache with initial data. If you would like to write data to an existing entry in the cache, pass in the entry's cache key to `id`. | -| `writeQuery({ query, variables, data })` | Similar to `writeData` (writes data to the root of the cache) but uses the specified query to validate that the shape of the data you’re writing to the cache is the same as the shape of the data required by the query. | +| `writeQuery({ query, variables, data })` | Writes data to the root of the cache using the specified query to validate that the shape of the data you’re writing to the cache is the same as the shape of the data required by the query. Great for prepping the cache with initial data. | | `readQuery({ query, variables })` | Read data from the cache for the specified query. | -| `writeFragment({ id, fragment, fragmentName, variables, data })` | Similar to `writeData` (writes data to an existing entry in the cache) but uses the specified fragment to validate that the shape of the data you’re writing to the cache is the same as the shape of the data required by the fragment. | +| `writeFragment({ id, fragment, fragmentName, variables, data })` | Similar to `writeQuery` (writes data to the cache) but uses the specified fragment to validate that the shape of the data you’re writing to the cache is the same as the shape of the data required by the fragment. | | `readFragment({ id, fragment, fragmentName, variables })` | Read data from the cache for the specified fragment. | From 1f5b5c5f87450ab27f7c188d17a4e11fc82b1b42 Mon Sep 17 00:00:00 2001 From: hwillson Date: Mon, 2 Mar 2020 10:45:03 -0500 Subject: [PATCH 3/3] Mention `writeData` removal in the Migration Guide --- docs/source/data/local-state.mdx | 2 +- .../migrating/apollo-client-3-migration.md | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/source/data/local-state.mdx b/docs/source/data/local-state.mdx index eb80f5c3cef..8e36ad18e8c 100644 --- a/docs/source/data/local-state.mdx +++ b/docs/source/data/local-state.mdx @@ -1361,7 +1361,7 @@ Updating your application to use Apollo Client's local state management features }); ``` -3. `defaults` are no longer supported. To prep the cache, use [`cache.writeQuery`](#writequery) directly instead. So +3. `defaults` are no longer supported. To prep the cache, use [`cache.writeQuery`](../caching/cache-interaction/#writequery-and-writefragment) directly instead. So ```js const cache = new InMemoryCache(); diff --git a/docs/source/migrating/apollo-client-3-migration.md b/docs/source/migrating/apollo-client-3-migration.md index c818619a2d5..a3cc45b3dd5 100644 --- a/docs/source/migrating/apollo-client-3-migration.md +++ b/docs/source/migrating/apollo-client-3-migration.md @@ -179,3 +179,25 @@ The following cache changes are **not** backward compatible. Take them into cons * All cache results are now frozen/immutable, as promised in the [Apollo Client 2.6 blog post](https://blog.apollographql.com/whats-new-in-apollo-client-2-6-b3acf28ecad1) ([PR #5153](https://github.com/apollographql/apollo-client/pull/5153)). * `FragmentMatcher`, `HeuristicFragmentMatcher`, and `IntrospectionFragmentMatcher` have all been removed. We recommend using the `InMemoryCache`’s `possibleTypes` option instead. For more information, see [Defining possibleTypes manually](../data/fragments/#defining-possibletypes-manually) ([PR #5073](https://github.com/apollographql/apollo-client/pull/5073)). * The internal representation of normalized data in the cache has changed. If you’re using `apollo-cache-inmemory`’s public API, then these changes shouldn’t impact you. If you are manipulating cached data directly instead, review [PR #5146](https://github.com/apollographql/apollo-client/pull/5146) for details. +* `client|cache.writeData` have been fully removed. `client|cache.writeQuery`, `client|cache.writeFragment`, and/or `cache.modify` can be used to update the cache. For example: + + ```js + client.writeData({ + data: { + cartItems: [] + } + }); + ``` + + can be converted to: + + ```js + client.writeQuery({ + query: gql`{ cartItems }`, + data: { + cartItems: [] + } + }); + ``` + + For more details around why `writeData` has been removed, see [PR #5923](https://github.com/apollographql/apollo-client/pull/5923).