diff --git a/docs/package-lock.json b/docs/package-lock.json index 875e2135987..7bc04940077 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -15273,9 +15273,9 @@ } }, "prismjs": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.26.0.tgz", - "integrity": "sha512-HUoH9C5Z3jKkl3UunCyiD5jwk0+Hz0fIgQ2nbwU2Oo/ceuTAQAg+pPVnfdt2TJWRVLcxKh9iuoYDUSc8clb5UQ==" + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==" }, "probe-image-size": { "version": "6.0.0", @@ -18453,16 +18453,36 @@ } }, "typedoc": { - "version": "0.22.12", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.12.tgz", - "integrity": "sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==", + "version": "0.22.13", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.13.tgz", + "integrity": "sha512-NHNI7Dr6JHa/I3+c62gdRNXBIyX7P33O9TafGLd07ur3MqzcKgwTvpg18EtvCLHJyfeSthAtCLpM7WkStUmDuQ==", "dev": true, "requires": { "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^4.0.10", - "minimatch": "^3.0.4", - "shiki": "^0.10.0" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "typescript": { @@ -19052,9 +19072,9 @@ } }, "vscode-oniguruma": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz", - "integrity": "sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz", + "integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==", "dev": true }, "vscode-textmate": { diff --git a/docs/package.json b/docs/package.json index d5199873afc..50988e5a109 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,7 +13,7 @@ "react-dom": "16.14.0" }, "devDependencies": { - "typedoc": "0.22.12", + "typedoc": "0.22.13", "typescript": "4.4.4" } } diff --git a/docs/source/caching/cache-configuration.mdx b/docs/source/caching/cache-configuration.mdx index f1e9631195a..2865ed4bb0d 100644 --- a/docs/source/caching/cache-configuration.mdx +++ b/docs/source/caching/cache-configuration.mdx @@ -9,7 +9,7 @@ This article describes cache setup and configuration. To learn how to interact w ## Installation -As of Apollo Client 3.0, the `InMemoryCache` class is provided by the `@apollo/client` package. No additional libraries are required. +In Apollo Client 3, the `InMemoryCache` class is provided by the `@apollo/client` package. No additional libraries are required. ## Initialization @@ -28,16 +28,15 @@ The `InMemoryCache` constructor accepts a variety of [configuration options](#co ## Configuration options -Although the cache's default behavior is suitable for a wide variety of applications, you can configure its behavior to better suit your particular use case. In particular, you can: +You can configure the cache's behavior to better suit your application. For example, you can: -* Specify custom primary key fields -* Customize the storage and retrieval of individual fields -* Customize the interpretation of field arguments -* Define supertype-subtype relationships for fragment matching -* Define patterns for pagination -* Manage client-side local state +* Customize the format of a particular type's [cache ID](#customizing-cache-ids) +* Customize the storage and retrieval of [individual fields](./cache-field-behavior/) +* Define polymorphic type relationships for [fragment matching](#possibletypes) +* Define patterns for [pagination](../pagination/overview/) +* Manage client-side [local state](../local-state/local-state-management/) -To customize cache behavior, provide an `options` object to the `InMemoryCache` constructor. This object supports the following fields: +To customize cache behavior, you provide a configuration object to the `InMemoryCache` constructor. This object supports the following fields: @@ -139,7 +138,7 @@ Deprecated in favor of the `keyFields` option of the [`TypePolicy` object](#type ## Customizing cache IDs -You can customize how the `InMemoryCache` generates cache IDs for individual types in your schema ([see the default behavior](./overview/#2-generate-cache-ids)). This is helpful especially if a type uses a field (or fields!) _besides_ `id` or `_id` as its unique identifier. +You can customize how the `InMemoryCache` generates cache IDs for individual types in your schema ([see the default behavior](./overview/#2-generate-cache-ids)). This is helpful especially if a type uses a field (or fields!) _besides_ `id` or `_id` as its unique identifier. To accomplish this, you define a `TypePolicy` for each type you want to customize. You specify all of your cache's `typePolicies` in [the `options` object you provide to the `InMemoryCache` constructor](#configuration-options). @@ -179,7 +178,7 @@ This example shows a variety of `typePolicies` with different `keyFields`: * The `Book` type includes a _subfield_ as part of its cache ID. * The `["name"]` item indicates that the `name` field of the _previous_ field in the array (`author`) is part of the cache ID. The `Book`'s `author` field must be an object that includes a `name` field for this to be valid. * A valid cache ID for the `Book` type has the following structure: - ``` + ``` Book:{"title":"Fahrenheit 451","author":{"name":"Ray Bradbury"}} ``` * The `AllProducts` type illustrates a special strategy for a **singleton** type. If the cache will only ever contain one `AllProducts` object and that object has _no_ identifying fields, you can provide an empty array for its `keyFields`. @@ -219,7 +218,7 @@ Notice that the above function still uses different logic to generate keys based This code also has the following drawbacks: * It's sensitive to aliasing mistakes. -* It does nothing to protect against undefined `object` properties. +* It does nothing to protect against undefined object properties. * Accidentally using different key fields at different times can cause inconsistencies in the cache. ### Disabling normalization @@ -232,7 +231,7 @@ Objects that are not normalized are instead embedded within their _parent_ objec ## `TypePolicy` fields -To customize how the cache interacts with specific types in your schema, you can provide an object mapping `__typename` strings to `TypePolicy` objects when you create a new `InMemoryCache` object. +To customize how the cache interacts with specific types in your schema, you can pass the `InMemoryCache` constructor an object that maps `__typename` strings to `TypePolicy` objects. A `TypePolicy` object can include the following fields: @@ -273,7 +272,7 @@ type KeyFieldsFunction = ( ### Overriding root operation types (uncommon) -In addition to `keyFields`, a `TypePolicy` can indicate that it represents the root query, mutation, or subscription type by setting `queryType`, `mutationType`, or `subscriptionType` as `true`: +In addition to `keyFields`, a `TypePolicy` can indicate that its type represents the root query, mutation, or subscription type by setting `queryType`, `mutationType`, or `subscriptionType` to `true`: ```ts const cache = new InMemoryCache({ @@ -312,10 +311,10 @@ const equivalentResult = cache.readQuery({ }); ``` -The cache normally obtains `__typename` information by adding the `__typename` field to every query selection set it sends to the server. It could technically use the same trick for the outermost selection set of every operation, but the `__typename` of the root query or mutation is almost always `"Query"` or `"Mutation"`, so the cache assumes those common defaults unless instructed otherwise in a `TypePolicy`. +The cache normally obtains `__typename` information by adding the `__typename` field to every query selection set it sends to the server. It could technically use this same method for the outermost selection set of every operation, but the `__typename`s of the root query and mutation are almost always `"Query"` and `"Mutation"`, so the cache assumes those common defaults unless instructed otherwise in a `TypePolicy`. -Compared to the `__typename`s of entity objects like `Book` or `Person`, which are vital to proper identification and normalization, the `__typename` of the root query or mutation type is not nearly as useful or important, because those types are singletons with only one instance per client. +For most objects in a graph, the `__typename` field is vital for proper identification and normalization. For the root query and mutation types, the `__typename` is not nearly as useful or important, because those types are singletons with only one instance per client. ### The `fields` property -The final property within `TypePolicy` is the `fields` property, which is a map from string field names to `FieldPolicy` objects. For more information on this field, see [Customizing the behavior of cached fields](./cache-field-behavior). +The final property within `TypePolicy` is the `fields` property, which enables you to [customize the behavior of individual cached fields](./cache-field-behavior). diff --git a/docs/source/caching/cache-field-behavior.md b/docs/source/caching/cache-field-behavior.mdx similarity index 77% rename from docs/source/caching/cache-field-behavior.md rename to docs/source/caching/cache-field-behavior.mdx index f3f0d998d68..403bb696f9d 100644 --- a/docs/source/caching/cache-field-behavior.md +++ b/docs/source/caching/cache-field-behavior.mdx @@ -3,13 +3,15 @@ title: Customizing the behavior of cached fields sidebar_title: Customizing field behavior --- +import {ExpansionPanel} from 'gatsby-theme-apollo-docs'; + You can customize how a particular field in your Apollo Client cache is read and written. To do so, you define a **field policy** for the field. A field policy can include: * A [`read` function](#the-read-function) that specifies what happens when the field's cached value is read * A [`merge` function](#the-merge-function) that specifies what happens when field's cached value is written * An array of [key arguments](#specifying-key-arguments) that help the cache avoid storing unnecessary duplicate data. -You provide field policies to the constructor of `InMemoryCache`. Each field policy is defined inside whatever [`TypePolicy` object](./cache-configuration/#typepolicy-fields) corresponds to the type that contains the field. The following example defines a field policy for the `name` field of a `Person` type: +You provide field policies to the constructor of `InMemoryCache`. Each field policy is defined inside whichever [`TypePolicy` object](./cache-configuration/#typepolicy-fields) corresponds to the field's parent type. The following example defines a field policy for the `name` field of a `Person` type: ```ts{5-10} const cache = new InMemoryCache({ @@ -56,7 +58,9 @@ const cache = new InMemoryCache({ }); ``` -If a field accepts arguments, the second parameter includes the values of those arguments. The following `read` function checks to see if the `maxLength` argument is provided when the `name` field is queried. If it is, the function returns only the first `maxLength` characters of the person's name. Otherwise, the person's full name is returned. +If a field accepts arguments, the `read` function's second parameter includes the values of those arguments. + +The following `read` function checks whether the `maxLength` argument is provided when the `name` field is queried. If it is, the function returns only the first `maxLength` characters of the person's name. Otherwise, the person's full name is returned. ```ts const cache = new InMemoryCache({ @@ -99,7 +103,7 @@ const cache = new InMemoryCache({ Other use cases for a `read` function include: * Transforming cached data to suit your client's needs, such as rounding floating-point values to the nearest integer -* Deriving local-only fields from one or more schema fields on the same object (such as deriving an `age` field from a `birthDate` field) +* Deriving [local-only fields](../local-state/managing-state-with-field-policies/) from one or more schema fields on the same object (such as deriving an `age` field from a `birthDate` field) * Deriving local-only fields from one or more schema fields across _multiple_ objects For a full list of the options provided to the `read` function, see the [API reference](#fieldpolicy-api-reference). You will almost never need to use all of these options, but each one has an important role when reading fields from the cache. @@ -134,102 +138,106 @@ Note that `existing` is undefined the very first time this function is called fo ### Merging non-normalized objects -Another common use case for custom field `merge` functions is to combine nested objects that do not have IDs, but are known (by you, the application developer) to represent the same logical object, assuming the parent object is the same. +You can use a `merge` function to intelligently combine nested objects that are _not_ normalized in your cache, assuming those objects are nested within the same normalized parent. + + + +```ts{6-8} +const cache = new InMemoryCache({ + typePolicies: { + Book: { + fields: { + author: { // Non-normalized Author object within Book + merge(existing, incoming, { mergeObjects }) { + return mergeObjects(existing, incoming); + }, + }, + }, + }, + }, +}); +``` + + -Suppose that a `Book` type has an `author` field, which is an object containing information like the author's `name`, `language`, and `dateOfBirth`. The `Book` object has `__typename: "Book"` and a unique `isbn` field, so the cache can tell when two `Book` result objects represent the same logical entity. However, for whatever reason, the query that retrieved this `Book` did not ask for enough information about the `book.author` object. Perhaps no `keyFields` were specified for the `Author` type, and there is no default `id` field. +#### Example -This lack of identifying information poses a problem for the cache, because it cannot determine automatically whether two `Author` result objects are the same. If multiple queries ask for different information about the author of this `Book`, the order of the queries matters, because the `favoriteBook.author` object from the second query cannot be safely merged with the `favoriteBook.author` object from the first query, and vice-versa: +Let's say our graph's schema includes the following types: ```graphql +type Book { + id: ID! + title: String! + author: Author! +} + +type Author { # Has no key fields + name: String! + dateOfBirth: String! +} + +type Query { + favoriteBook: Book! +} +``` + +With this schema, our cache can normalize `Book` objects because they have an `id` field. However, `Author` objects have no `id` field, and they also have no _other_ fields that can uniquely identify a particular instance. Therefore, the cache _can't_ normalize `Author` objects, and it can't tell when two different `Author` objects actually represent the _same_ author. + +Now, let's say our client executes the following two queries, in order: + +```graphql{5,14} query BookWithAuthorName { favoriteBook { - isbn - title + id author { name } } } -query BookWithAuthorLanguage { +query BookWithAuthorBirthdate { favoriteBook { - isbn - title + id author { - language + dateOfBirth } } } ``` -In such situations, the cache defaults to _replacing_ the existing `favoriteBook.author` data with the incoming data, without merging the `name` and `language` fields together, because the risk of merging inconsistent `name` and `language` fields from different authors is unacceptable. - -> Note: Apollo Client 2.x would sometimes merge unidentified objects. While this behavior might accidentally have aligned with the intentions of the developer, it led to subtle inconsistencies within the cache. Apollo Client 3.0 refuses to perform unsafe merges, and instead warns about potential loss of unidentified data. - -You could fix this problem by modifying your queries to request an `id` field for the `favoriteBook.author` objects, or by specifying custom `keyFields` in the `Author` type policy, such as `["name", "dateOfBirth"]`. Providing the cache with this information allows it to know when two `Author` objects represent the same logical entity, so it can safely merge their fields. This solution is recommended, when feasible. - -However, you may encounter situations where your graph does not provide any uniquely identifying fields for `Author` objects. In these rare scenarios, it might be safe to assume that a given `Book` has one and only one primary `Author`, and the author never changes. In other words, the identity of the author is implied by the identity of the book. This common-sense knowledge is something you have at your disposal, as a human, but it must be communicated to the cache, which is neither human nor capable of telepathy. +When the _first_ query returns, Apollo Client writes a `Book` object like the following to the cache: -In such situations, you can define a custom `merge` function for the `author` field within the type policy for `Book`: - -```ts -const cache = new InMemoryCache({ - typePolicies: { - Book: { - fields: { - author: { - merge(existing, incoming) { - // Better, but not quite correct. - return { ...existing, ...incoming }; - }, - }, - }, - }, - }, -}); +```json +{ + "__typename": "Book", + "id": "abc123", + "author": { + "__typename": "Author", + "name": "George Eliot" + } +} ``` -Alternatively, if you prefer to keep the default behavior of completely replacing the `existing` data with the `incoming` data, while also silencing the warnings, the following `merge` function will explicitly permit replacement: - -```ts -const cache = new InMemoryCache({ - typePolicies: { - Book: { - fields: { - author: { - merge(existing, incoming) { - // Equivalent to what happens if there is no custom merge function. - return incoming; - }, - }, - }, - }, - }, -}); -``` +> Remember that because `Author` objects can't be normalized, they're nested directly within their parent object. -Since writing this kind of `merge` function can become repetitive, the following shorthand will provide the same behavior: +Now, when the _second_ query returns, the cached `Book` object is updated to the following: -```ts -const cache = new InMemoryCache({ - typePolicies: { - Book: { - fields: { - author: { - // Short for always preferring incoming over existing data. - merge: false, - }, - }, - }, - }, -}); +```json{6} +{ + "__typename": "Book", + "id": "abc123", + "author": { + "__typename": "Author", + "dateOfBirth": "1819-11-22" + } +} ``` -When you use `{ ...existing, ...incoming }`, `Author` objects with differing fields (`name`, `dateOfBirth`) can be combined without losing fields, which is definitely an improvement over blind replacement. +The `Author`'s `name` field has been removed! This is because Apollo Client can't be sure that the `Author` objects returned by the two queries actually refer to the same author. So instead of merging fields of the two objects, Apollo Client completely _overwrites_ the object (and logs a warning). -But what if the `Author` type defines its own custom `merge` functions for fields of the `incoming` object? Since we're using [object spread syntax](https://2ality.com/2016/10/rest-spread-properties.html), such fields will immediately overwrite fields in `existing`, without triggering any nested `merge` functions. The `{ ...existing, ...incoming }` syntax may be an improvement, but it is not fully correct. +However, _we_ are confident that these two objects represent the same author, because a book's author virtually never changes. Therefore, we can tell the cache to treat `Book.author` objects as the _same_ object as long as they belong to the same `Book`. This enables the cache to merge the `name` and `dateOfBirth` fields returned by different queries above. -Fortunately, you can find a helper function called `options.mergeObjects` in the options passed to the `merge` function, which generally behaves the same as `{ ...existing, ...incoming }`, except when the `incoming` fields have custom `merge` functions. When `options.mergeObjects` encounters custom `merge` functions for any of the fields in its second argument (`incoming`), those nested `merge` functions will be called before combining the fields of `existing` and `incoming`, as desired: +To achieve this, we can define a custom `merge` function for the `author` field within the type policy for `Book`: ```ts const cache = new InMemoryCache({ @@ -238,7 +246,6 @@ const cache = new InMemoryCache({ fields: { author: { merge(existing, incoming, { mergeObjects }) { - // Correct, thanks to invoking nested merge functions. return mergeObjects(existing, incoming); }, }, @@ -248,9 +255,9 @@ const cache = new InMemoryCache({ }); ``` -Because this `Book.author` field policy has no `Book`- or `Author`-specific logic in it, you can reuse this `merge` function for any field that needs this kind of handling. +Here, we use the `mergeObjects` helper function to merge values from the `existing` and `incoming` `Author` objects. It's important to use `mergeObjects` here instead of merging the objects with [object spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax), because `mergeObjects` makes sure to call any defined `merge` functions for _subfields_ of `Book.author`. -Since writing this kind of `merge` function can become repetitive, the following shorthand will provide the same behavior: +Notice that this `merge` function has zero `Book`- or `Author`-specific logic in it! This means you can reuse it for any number of non-normalized object fields. And because this exact `merge` function definition is so common, you can also define it with the following shorthand: ```ts const cache = new InMemoryCache({ diff --git a/docs/source/caching/cache-interaction.mdx b/docs/source/caching/cache-interaction.mdx index 69d140aaad3..c97a964cd77 100644 --- a/docs/source/caching/cache-interaction.mdx +++ b/docs/source/caching/cache-interaction.mdx @@ -109,7 +109,7 @@ Note the following about `writeQuery`: * Any changes you make to cached data with `writeQuery` are **not** pushed to your GraphQL server. If you reload your environment, these changes disappear. * The shape of your query is _not_ enforced by your GraphQL server's schema: * The query can include fields that are _not_ present in your schema. - * You can (but usually shouldn't) provide values for schema fields that are _invalid_ according to your schema. + * You can (but usually **shouldn't**) provide values for schema fields that are _invalid_ according to your schema. #### Editing existing data diff --git a/docs/source/caching/garbage-collection.mdx b/docs/source/caching/garbage-collection.mdx index 09894a23a36..3e0bcc69db7 100644 --- a/docs/source/caching/garbage-collection.mdx +++ b/docs/source/caching/garbage-collection.mdx @@ -15,7 +15,7 @@ The `gc` method removes all objects from the normalized cache that are not **rea cache.gc(); ``` -To determine whether an object is reachable, the cache starts from all known root objects and uses a tracing strategy to recursively visit all available child references. Any normalized objects that are _not_ visited during this process are removed. The `cache.gc()` method returns a list of the IDs of the removed objects. +To determine whether an object is reachable, the cache starts from all known root objects (usually `Query` and/or `Mutation`) and uses a tracing strategy to recursively visit all available child references. Any normalized objects that are _not_ visited during this process are removed. The `cache.gc()` method returns a list of the IDs of the removed objects. In addition to pruning your GraphQL data, `cache.gc` can also release memory that the cache uses to preserve unchanged parts of previous cache results: diff --git a/docs/source/caching/overview.mdx b/docs/source/caching/overview.mdx index 9d2efd52296..b8c46745ded 100644 --- a/docs/source/caching/overview.mdx +++ b/docs/source/caching/overview.mdx @@ -4,9 +4,7 @@ description: Overview sidebar_title: Overview --- -import { - ExpansionPanel, -} from 'gatsby-theme-apollo-docs/src/components/expansion-panel'; +import {ExpansionPanel} from 'gatsby-theme-apollo-docs'; Apollo Client stores the results of your GraphQL queries in a local, [normalized](#data-normalization), in-memory cache. This enables Apollo Client to respond almost immediately to queries for already-cached data, without even sending a network request. @@ -36,7 +34,7 @@ The Apollo Client cache is highly configurable. You can customize its behavior f ## How is data stored? -Apollo Client's `InMemoryCache` maintains a **flat lookup table** of objects that can reference each other. These objects accumulate field information from objects that are returned by your GraphQL queries. A single cached object might include fields returned by multiple queries, if those queries fetch _different_ fields of the _same_ object. +Apollo Client's `InMemoryCache` stores data as a **flat lookup table** of objects that can reference each other. These objects correspond to the objects that are returned by your GraphQL queries. A single cached object might include fields returned by multiple queries, if those queries fetch _different_ fields of the _same_ object. The cache is flat, but objects returned by a GraphQL query often _aren't_! In fact, their nesting can be arbitrarily deep. Take a look at this example query response: @@ -59,7 +57,7 @@ The cache is flat, but objects returned by a GraphQL query often _aren't_! In fa This response contains a `Person` object, which in turn contains a `Planet` object in its `homeworld` field. -So how does the `InMemoryCache` store _hierarchical_ data in a _flat_ lookup table? Before storing this data, the cache needs to **normalize** it. +So how does the `InMemoryCache` store _nested_ data in a _flat_ lookup table? Before storing this data, the cache needs to **normalize** it. ### Data normalization @@ -67,11 +65,14 @@ Whenever the Apollo Client cache receives query response data, it does the follo #### 1. Identify objects -First, the cache identifies all of the distinct objects included in a query response. In [the example above](#how-is-data-stored), there are two objects: a `Person` with `id` `cGVvcGxlOjE=`, and a `Planet` with `id` `cGxhbmV0czox`. +First, the cache identifies all of the distinct objects included in a query response. In [the example above](#how-is-data-stored), there are two objects: + +* A `Person` with `id` `cGVvcGxlOjE=` +* A `Planet` with `id` `cGxhbmV0czox` #### 2. Generate cache IDs -Second, the cache generates a **cache ID** for each identified object. A cache ID uniquely identifies a particular object while it's in the `InMemoryCache`. +After identifying all objects, the cache generates a **cache ID** for each one. A cache ID uniquely identifies a particular object while it's in the `InMemoryCache`. By default, an object's cache ID is the concatenation of the object's `__typename` and `id` (or `_id`) fields, separated by a colon (`:`). @@ -84,11 +85,11 @@ So, the default cache IDs for the objects in [the example above](#how-is-data-st > You can customize the cache ID format for a particular object type. See [Customizing cache IDs](./cache-configuration/#customizing-cache-ids). -If the cache _can't_ generate a cache ID for a particular object (for example, if no `__typename` field is present), that object is cached directly inside its _parent_ object and it must be referenced via the parent (this means the cache isn't always _completely_ flat). +If the cache _can't_ generate a cache ID for a particular object (for example, if no `id` or `_id` field is present), that object is cached directly inside its _parent_ object, and it must be referenced via the parent (this means the cache isn't always _completely_ flat). #### 3. Replace object fields with references -Third, the cache takes each field that contains an object and replaces its value with a **reference** to the appropriate object. +Next, the cache takes each field that contains an object and replaces its value with a **reference** to the appropriate object. For example, here's the `Person` object from the example above _before_ reference replacement: @@ -105,7 +106,7 @@ For example, here's the `Person` object from the example above _before_ referenc } ``` -And here it is _after_ replacement: +And here's that same object _after_ replacement: ```json{5-7} { @@ -126,9 +127,9 @@ Later, if you query for _another_ `Person` who has the same `homeworld`, that no #### 4. Store normalized objects -The resulting objects are all stored in the cache's flat lookup table. +Finally, the resulting objects are all stored in the cache's flat lookup table. -Whenever an incoming object has the same cache ID as an _existing_ cached object, the fields of those objects are _merged_: +Whenever an incoming object has the same cache ID as an _existing_ cached object, the fields of those objects are _merged:_ * If the incoming object and the existing object share any fields, the incoming object _overwrites_ the cached values for those fields. * Fields that appear in _only_ the existing object or _only_ the incoming object are preserved. @@ -209,7 +210,7 @@ This query returns the following result of three `Person` objects, each with a c -> Notice that each object in the result includes a `__typename` field, even though our query string _didn't_ include this field. That's because Apollo Client _automatically_ queries for every object's `__typename` field. +> Notice that each object in the result includes a `__typename` field, even though our query string _didn't_ include this field. That's because Apollo Client _automatically_ queries for every object's `__typename`. After the result is cached, we can view the state of our cache in the Apollo Client Devtools: diff --git a/docs/source/data/queries.mdx b/docs/source/data/queries.mdx index 22aca4e5f84..7257574cd79 100644 --- a/docs/source/data/queries.mdx +++ b/docs/source/data/queries.mdx @@ -10,7 +10,7 @@ This article shows how to fetch GraphQL data in React with the `useQuery` hook a ## Prerequisites -This article assumes you're familiar with building basic GraphQL queries. If you need a refresher, we recommend [this guide](http://graphql.org/learn/queries/). You can also build example queries against Apollo's [full-stack tutorial server](https://apollo-fullstack-tutorial.herokuapp.com/). +This article assumes you're familiar with building basic GraphQL queries. If you need a refresher, we recommend [this guide](http://graphql.org/learn/queries/). You can also build example queries against Apollo's [full-stack tutorial server](https://apollo-fullstack-tutorial.herokuapp.com/graphql). This article also assumes that you've already set up Apollo Client and have wrapped your React app in an `ApolloProvider` component. For more information, see the [getting started guide](../get-started/). @@ -67,7 +67,7 @@ In the next step, we'll associate the dropdown with a more sophisticated query t ## Caching query results -Whenever Apollo Client fetches query results from your server, it automatically **caches** those results locally. This makes subsequent executions of the same query extremely fast. +Whenever Apollo Client fetches query results from your server, it automatically **caches** those results locally. This makes later executions of that same query extremely fast. To see this caching in action, let's build a new component called `DogPhoto`. `DogPhoto` accepts a prop called `breed` that reflects the current value of the dropdown menu in our `Dogs` component: @@ -97,7 +97,7 @@ function DogPhoto({ breed }) { Notice that we're providing a configuration option (`variables`) to the `useQuery` hook this time. The `variables` option is an object that contains all of the variables we want to pass to our GraphQL query. In this case, we want to pass the currently selected `breed` from the dropdown. -Select `bulldog` from the dropdown to see its photo appear. Then switch to another breed, and then switch _back_ to `bulldog`. You'll notice that the bulldog photo loads instantly the second time around. This is the Apollo cache at work! +Select `bulldog` from the dropdown to see its photo appear. Then switch to another breed, and then switch _back_ to `bulldog`. You'll notice that the bulldog photo loads instantly the second time around. This is the cache at work! Next, let's learn some techniques for ensuring that our cached data is fresh. @@ -109,7 +109,7 @@ Sometimes, you want to make sure that your query's cached data is up to date wit Polling provides near-real-time synchronization with your server by executing your query periodically at a specified interval. To enable polling for a query, pass a `pollInterval` configuration option to the `useQuery` hook with an interval in milliseconds: -```jsx:title=index.js +```jsx{4}:title=index.js function DogPhoto({ breed }) { const { loading, error, data } = useQuery(GET_DOG_PHOTO, { variables: { breed }, @@ -182,7 +182,7 @@ Let's return to our refetching example from the previous section. If you click t The `useQuery` hook's result object provides fine-grained information about the status of the query via the `networkStatus` property. To take advantage of this information, we set the `notifyOnNetworkStatusChange` option to `true` so our query component re-renders while a refetch is in flight: -```jsx:title=index.js +```jsx{4,8,12}:title=index.js import { NetworkStatus } from '@apollo/client'; function DogPhoto({ breed }) { diff --git a/docs/source/development-testing/client-schema-mocking.mdx b/docs/source/development-testing/client-schema-mocking.mdx index 2a794fa9415..46fa7d2579d 100644 --- a/docs/source/development-testing/client-schema-mocking.mdx +++ b/docs/source/development-testing/client-schema-mocking.mdx @@ -66,10 +66,10 @@ const cache = new InMemoryCache({ }); ``` -This enables us to query the field, but we might not want to show the same boilerplate description for every rocket. To add variety to our mocked output, we can use a library like [faker.js](https://github.com/marak/Faker.js/): +This enables us to query the field, but we might not want to show the same boilerplate description for every rocket. To add variety to our mocked output, we can use a library like [faker.js](https://github.com/faker-js/faker): ```js:title=index.js -import faker from "faker/locale/en"; +import { faker } from "@faker-js/faker"; // Returns 1 or 2 sentences of Lorem Ipsum const oneOrTwoSentences = () => diff --git a/docs/source/migrating/apollo-client-3-migration.mdx b/docs/source/migrating/apollo-client-3-migration.mdx index d133f41cb13..42c4be4517b 100644 --- a/docs/source/migrating/apollo-client-3-migration.mdx +++ b/docs/source/migrating/apollo-client-3-migration.mdx @@ -56,6 +56,12 @@ import { ApolloProvider, useQuery, useApolloClient } from '@apollo/client' As part of migrating, we recommend removing all `@apollo/react-hooks` dependencies. +**Breaking Changes:** + +* `useQuery` [no longer maintains the previously fetched results](https://github.com/apollographql/apollo-client/pull/6566) in its `data` result when loading new data. Instead, when new data is being loaded (i.e. `loading` === `true`) the `data` result of `useQuery` is set to `undefined`. Use the `previousData` result as a bridge to the old v2 behavior. +* `refetch` functionality of `useQuery` [was broken in v3.5.x until it was fixed in v3.5.8](https://github.com/apollographql/apollo-client/issues/9101). Previous to this version, if the `skip: true` option was used, `refetch` would always be `undefined`. + + ### @apollo/react-ssr React Apollo’s SSR utilities (like `getDataFromTree`, `getMarkupFromTree`, and `renderToStringWithData`) are included in the `@apollo/client` package. Access them via `@apollo/client/react/ssr`: diff --git a/docs/source/pagination/key-args.mdx b/docs/source/pagination/key-args.mdx index c25179a086c..848bebed9aa 100644 --- a/docs/source/pagination/key-args.mdx +++ b/docs/source/pagination/key-args.mdx @@ -1,35 +1,272 @@ --- -title: The keyArgs API +title: Key arguments in Apollo Client +description: Using the keyArgs API sidebar_title: keyArgs --- -> We recommend reading [Core pagination API](./core-api) before learning about considerations specific to the `keyArgs` configuration. +> We recommend reading [Core pagination API](./core-api) before learning about considerations specific to `keyArgs` configuration. -In GraphQL, a single field within a single object may store multiple different values at once, corresponding to different combinations of field arguments passed to the field in a given request. This multiplicity of field values requires the cache to store the values separately, so that they can be retrieved separately in the future. +The Apollo Client cache can store multiple entries for a single schema field. By default, each entry corresponds to a different set of values for the field's arguments. -There are many ways this storage could be structured, but `InMemoryCache` represents each entity object as a `StoreObject`, which is an ordinary JavaScript object with string keys generated from the name of the field plus the serialized arguments (if any), rather than using just the name of the field. Fields without arguments are keyed by their field names alone. +For example, consider this `Query.user` field: -By default, `InMemoryCache` incorporates _all_ field arguments into the storage key for each field, so a single field can simultaneously hold as many different values as the number of unique combinations of arguments. This default strategy sacrifices the hit rate of the cache in order to avoid reusing field values inappropriately when any of the arguments are not the same. +```graphql{3} +type Query { + # Returns whichever User object corresponds to `id` + user(id: ID!): User +} +``` + +If we query for `User`s with `id`s `1` and `2`, the Apollo Client cache stores entries for _both_ like so: + +```js{3,6}:title=Cache +{ + 'ROOT_QUERY': { + 'user({"id":"1"})': { + '__ref': 'User:1' + }, + 'user({"id":"2"})': { + '__ref': 'User:2' + } + } +} +``` + +As shown above, each entry's **storage key** includes the corresponding argument values. This means that if _any_ of a field's arguments differ between queries, the storage keys _also_ differ, and those queries result in distinct cache entries. + +> If a field has no arguments, its storage key is just its name. + +This default behavior is for safety: the cache doesn't know whether it can _merge_ the values returned for different argument combinations without invalidating data. In the example above, the cache definitely _shouldn't_ merge the results of querying for `User`s with `id`s `1` and `2`. + +## Pagination issues + +Certain arguments _shouldn't_ cause the Apollo Client cache to store a separate entry. This is almost always the case for arguments related to paginated lists. + +Consider this `Query.feed` field: + +```graphql{2} +type Query { + feed(offset: Int, limit: Int, category: Category): [FeedItem!] +} +``` + +The `offset` and `limit` arguments enable a client to specify which "page" of the feed it wants to fetch. In an app with an infinitely scrolling feed, the client might initially fetch the first ten items, then fetch the _next_ ten: + +```graphql +# First query +query GetFeedItems { + feed(offset: 0, limit: 10, category: "SPORTS") +} + +# Second query +query GetFeedItems { + feed(offset: 10, limit: 10, category: "SPORTS") +} +``` + +But because their argument values differ, these two lists of ten items are cached _separately_ by default. This means that when the second query completes, the returned items _aren't_ appended to the original list in the feed! + +```js{3-4,10-11}:title=Cache +{ + 'ROOT_QUERY': { + // First query + 'feed({"offset":"0","limit":"10","category":"SPORTS"})': [ + { + '__ref': 'FeedItem:1' + }, + // ...additional items... + ], + // Second query + 'feed({"offset":"10","limit":"10","category":"SPORTS"})': [ + { + '__ref': 'FeedItem:11' + }, + // ...additional items... + ] + } +} +``` + +In this case, we _don't_ want `offset` or `limit` to be included in a cache entry's storage key. Instead, we want the cache to _merge_ the results of the two above queries into a single cache entry that includes the items from both lists. + +To help handle this case, we can [set key arguments](#setting-keyargs) for the field. + +## Setting `keyArgs` + +A **key argument** is an argument for a GraphQL field that's included in cache storage keys for that field. By default, _all_ GraphQL arguments are key arguments, as shown in our feed example: + +```js{3-4,10-11}:title=Cache +{ + 'ROOT_QUERY': { + // First query + 'feed({"offset":"0","limit":"10","category":"SPORTS"})': [ + { + '__ref': 'FeedItem:1' + }, + // ...additional items... + ], + // Second query + 'feed({"offset":"10","limit":"10","category":"SPORTS"})': [ + { + '__ref': 'FeedItem:11' + }, + // ...additional items... + ] + } +} +``` + +You can override this default behavior by defining a cache [field policy](../caching/cache-field-behavior) for a particular field: + +```js{5-7} +const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + feed: { + keyArgs: ["category"], + }, + }, + }, + }, +}); +``` + +This field policy for `Query.feed` includes a `keyArgs` array, which contains the names of all arguments that the cache _should_ include in its storage keys. + +In this case, we don't want the cache to treat `offset` or `limit` as key arguments, because those arguments don't change _which_ list we're fetching from. However, we _do_ want to treat `category` as a key argument, because we want to store our `SPORTS` feed separately from other feeds (such as `FASHION` or `MUSIC`). + +After setting `keyArgs` as shown, we end up with a _single_ cache entry for our `SPORTS` feed (note the absence of `offset` and `limit` in the storage key): -However, in many cases, you may realize that your field values are more reusable than this default strategy assumes, and some or all of the arguments are not actually relevant to the storage identity of the field. Fortunately, this system is configurable. +```js:title=Cache +{ + 'ROOT_QUERY': { + 'feed({"category":"SPORTS"})': [ + { + '__ref': 'FeedItem:1' + }, + // ...additional items from first query... + { + '__ref': 'FeedItem:11' + }, + // ...additional items from second query... + ] + } +} +``` + +> **Important:** After you define `keyArgs` for a paginated list field like `Query.feed`, you also need to [define a `merge` function](./core-api/#defining-a-field-policy) for the field. Otherwise, the list returned by the second query will _overwrite_ the first list instead of merging with it. + +## Supported values for `keyArgs` + +You can provide the following values for a field's `keyArgs`: + +* `false` (indicates that the field has _no_ key arguments) +* [An array](#keyargs-array) of argument, directive, and variable names +* [A function](#keyargs-function) (advanced) + +### `keyArgs` array + +A `keyArgs` array can include the types of values shown below. The storage key for a cached field uses the values of _all_ arguments, directives, and variables included in the array. + +* Argument names: + + ```js + // Here, category and id are two arguments of the field + ["category", "id"] + ``` + +* _Nested_ argument names for input types with subfields: -In addition to `merge` and `read` functions, `InMemoryCache` field policies can contain a configuration called [`keyArgs`](../caching/cache-field-behavior#specifying-key-arguments), which specifies an array of argument names whose values should be serialized and appended to the field name to create a distinct storage key for a particular value of the field to be stored in the cache. + ```js + // Here, details is an input type argument + // with subfields name and date + ["details", ["name", "date"] ] + ``` -A `keyArgs: ["type"]` field policy configuration means `type` is the only argument the cache should consider (in addition to the field name and the identity of the enclosing object) when accessing values for this field. A `keyArgs: false` configuration disables the whole system of differentiating field values by arguments, so the field's value will be identified only by the field's name (within some `StoreObject`), without any serialized arguments appended to it. +* Directive names (indicated with `@`), optionally with one or more of their arguments: -> In the unlikely event that a `keyArgs` array is insufficient to specify the storage key, you can alternatively pass a function for `keyArgs`, which allows you to sanitize and serialize the `args` object however you like. You can also provide nested `keyArgs` in the format `["arg", ["nested-arg"]]`. + ```js + // Here, @units is a directive that can be applied + // to the field, and it has a type argument + ["@units", ["type"] ] + ``` -This article provides specific technical guidance on choosing appropriate `keyArgs` configurations, especially when working with paginated fields and field policies. +* Variable names (indicated with `$`): + + ```js + // Here, $userId is a variable that's provided to some + // operations that include the field + ["$userId"] + ``` + +### `keyArgs` function (advanced) + +You can define a completely different format for a field's storage key by providing a custom function to `keyArgs`. This function takes the field's arguments and other context as parameters, and it can return any string to use as the storage key (or a dynamically-generated `keyArgs` array). + +This is for advanced use cases. For details, see [`FieldPolicy` API reference](../caching/cache-field-behavior/#fieldpolicy-api-reference). ## Which arguments belong in `keyArgs`? -Throughout this area of the documentation, you'll find a number of possible `keyArgs` configurations, ranging from including all arguments by default, to completely disabling argument-based field identification using `keyArgs: false`. To understand which arguments belong in `keyArgs` (if any), it's helpful to consider those two extremes first—including all arguments in the field key, or none of them—because those are the most common cases. Building on that understanding, we can then discuss the consequences of moving an individual argument into or out of `keyArgs`. +When deciding which of a field's arguments to include in `keyArgs`, it's helpful to start by considering the two extremes: _all_ arguments and _no_ arguments. These initial options help to demonstrate the effects of adding or removing a single argument. + +### Using all arguments + +If all arguments are key arguments (this is the default behavior), every distinct combination of argument values for a field results in a distinct cache entry. In other words, changing any argument value results in a different storage key, so the returned value is stored separately. We see this in our pagination example: + +```js{3-4,10-11}:title=Cache +{ + 'ROOT_QUERY': { + // First query + 'feed({"offset":"0","limit":"10","category":"SPORTS"})': [ + { + '__ref': 'FeedItem:1' + }, + // ...additional items... + ], + // Second query + 'feed({"offset":"10","limit":"10","category":"SPORTS"})': [ + { + '__ref': 'FeedItem:11' + }, + // ...additional items... + ] + } +} +``` + +With this approach, Apollo Client can't return a cached value for a field unless _all_ of the field's arguments match a previously cached result. This significantly reduces the cache's hit rate, but it also prevents the cache from returning an incorrect value when differences in arguments are relevant (as with our `User` example): -If you include all arguments in the field key, as `InMemoryCache` does by default, then every different combination of argument values will correspond to a different storage location for internal field data. In other words, if you change any argument values, the field key will be different, so the field value will be stored in a different location. In your `read` and `merge` functions, this internal field data is provided by the `existing` parameter, which will be undefined when a particular combination of arguments has never been seen before. With this approach, the cache can reuse field values only if the arguments exactly match, which significantly reduces the hit rate of the cache, but also keeps the cache from inappropriately reusing field values when differences in arguments actually matter. +```js{3,6}:title=Cache +{ + 'ROOT_QUERY': { + 'user({"id":"1"})': { + '__ref': 'User:1' + }, + 'user({"id":"2"})': { + '__ref': 'User:2' + } + } +} +``` + +### Using no arguments + +If no arguments are key arguments (you configure this by setting `keyArgs: false`), the field's storage key is just the field's name, without any argument values appended to it. This means that by default, whenever a query returns a value for that field, that value _replaces_ whatever value was already in the cache. + +This default behavior is often undesirable (especially for a paginated list), so you can define `read` and `merge` functions that use argument values to determine how a newly returned value is combined with an _existing_ cached value. -On the other hand, if you configure your field with `keyArgs: false`, the field key will always be just the field name, without any extra characters appended to it. Because your `read` and `merge` functions have access to the field arguments via `options.args`, you could use `read` and `merge` to keep your internal data separated according to the arguments, simulating the behavior of `keyArgs` without actually using `keyArgs`. Your `read` function then gets to decide whether an existing field value can be reused, and how it should be transformed before it is reused, based on the runtime argument values and whatever internal value was previously stored. +#### Example -For example, we could have used `keyArgs: false` instead of `keyArgs: ["type"]` for our `Query.feed` field policy: +Recall this `Query.feed` field from [Pagination issues](#pagination-issues): + +```graphql{2} +type Query { + feed(offset: Int, limit: Int, category: Category): [FeedItem!] +} +``` + +We originally set `keyArgs: ["category"]` for this field to keep feed items from different categories separate. We can achieve the same behavior by setting `keyArgs: false` and defining the following `read` and `merge` functions: ```js const cache = new InMemoryCache({ @@ -39,17 +276,16 @@ const cache = new InMemoryCache({ feed: { keyArgs: false, - read(existing = {}, { args: { type, offset, limit }}) { - return existing[type] && - existing[type].slice(offset, offset + limit); + read(existing = {}, { args: { offset, limit, category }}) { + return existing[category]?.slice(offset, offset + limit); }, - merge(existing = {}, incoming, { args: { type, offset = 0 }}) { - const merged = existing[type] ? existing[type].slice(0) : []; + merge(existing = {}, incoming, { args: { category, offset = 0 }}) { + const merged = existing[category] ? existing[category].slice(0) : []; for (let i = 0; i < incoming.length; ++i) { merged[offset + i] = incoming[i]; } - existing[type] = merged; + existing[category] = merged; return existing; }, }, @@ -59,25 +295,30 @@ const cache = new InMemoryCache({ }); ``` -Instead of a single array, `existing` is now a map from `type`s to feeds, allowing a single field value to store multiple feed arrays, separated by `type`. However, this manual separation is logically equivalent to what would happen if you moved the `type` argument into `keyArgs` (using `keyArgs: ["type"]`, as above), so the extra effort is probably unnecessary. Assuming feeds with different `type` values have different data, and assuming our `read` function does not need simultaneous access to multiple feeds of different types, we can safely shift the responsibility for handling the `type` argument from the `read` and `merge` functions back to `keyArgs`, and simplify `read` and `merge` to handle only one feed at a time. +With the code above, the value of the `existing` cached value passed to our `read` and `merge` functions is a _map_ of `category` names to `FeedItem` lists. This map enables our single cached field value to store multiple distinct lists. This manual separation is logically equivalent to using `keyArgs: ["category"]`, so the extra effort is often unnecessary. -In short, if the logic for storing and retrieving field data is the same for different values of a given argument (like `type`), and those field values are logically independent from one another, then you probably should move that argument into `keyArgs`, to save yourself from having to deal with it in your `read` and `merge` functions. +If we know that feeds with different `category` values have different data, _and_ we know that our `read` function never needs simultaneous access to _multiple_ category feeds, we can safely shift the responsibility for the `category` argument to `keyArgs`. This enables us to simplify our `read` and `merge` functions to handle only one feed at a time. -By contrast, arguments that limit, filter, sort, or otherwise reprocess existing field data usually do not belong in `keyArgs`, because putting them in `keyArgs` makes field storage keys more diverse, reducing cache hit rate and limiting your ability to use different arguments to retrieve different views of the same data (without making a additional network requests). +### Summary -As a general rule, `read` and `merge` functions can do almost anything with your field data, but there might be a less powerful tool (like `keyArgs`) that allows you to simplify (or avoid writing) custom `read` or `merge` functions. Whenever you have a choice between two capable tools, you should prefer the one that minimizes the total complexity of your code, which often favors a more limited, declarative API like `keyArgs`, over the unlimited power of functions like `merge` or `read`. +If the logic for storing and retrieving a field's data is identical for different values of a given argument (like `category` above), and the distinct field values are logically independent from one another, then you should probably add that argument to `keyArgs` to avoid handling it in your `read` and `merge` functions. + +By contrast, arguments that limit, filter, sort, or otherwise reprocess existing field data usually do _not_ belong in `keyArgs`. This is because putting them in `keyArgs` makes storage keys more diverse, reducing cache hit rate and limiting your ability to use different arguments to retrieve different views of the same data. + +As a general rule, `read` and `merge` functions can do almost anything with your cached field data, but `keyArgs` often provide similar functionality with less code complexity. Whenever possible you should prefer the limited, declarative API of `keyArgs` over the unlimited power of functions like `merge` and `read`. ## The `@connection` directive -The `@connection` directive is a Relay-inspired convention that Apollo Client supports, though we now recommend `keyArgs` instead, because you can achieve the same effect with a single `keyArgs` configuration, whereas the `@connection` directive needs to be repeated in every query you send to your server. +The `@connection` directive is a Relay-inspired convention that Apollo Client supports. However, we recommend using `keyArgs` instead, because you can achieve the same effect with a single `keyArgs` configuration, whereas you need to include the `@connection` directive in every query you send to your server. In other words, whereas Relay encourages the following `@connection(...)` directive for `Query.feed` queries: + ```js const FEED_QUERY = gql` - query Feed($type: FeedType!, $offset: Int, $limit: Int) { - feed(type: $type, offset: $offset, limit: $limit) @connection( + query Feed($category: FeedCategory!, $offset: Int, $limit: Int) { + feed(category: $category, offset: $offset, limit: $limit) @connection( key: "feed", - filter: ["type"] + filter: ["category"] ) { edges { node { ... } @@ -90,11 +331,13 @@ const FEED_QUERY = gql` } `; ``` -in Apollo Client, you would typically use the following query (the same query without the `@connection(...)` directive): + +in Apollo Client, you can use the following query (the same query without the `@connection(...)` directive): + ```js const FEED_QUERY = gql` - query Feed($type: FeedType!, $offset: Int, $limit: Int) { - feed(type: $type, offset: $offset, limit: $limit) { + query Feed($category: FeedCategory!, $offset: Int, $limit: Int) { + feed(category: $category, offset: $offset, limit: $limit) { edges { node { ... } } @@ -106,14 +349,16 @@ const FEED_QUERY = gql` } `; ``` + and instead configure `keyArgs` in your `Query.feed` field policy: + ```js const cache = new InMemoryCache({ typePolicies: { Query: { fields: { feed: { - keyArgs: ["type"], + keyArgs: ["category"], }, }, }, @@ -121,7 +366,8 @@ const cache = new InMemoryCache({ }) ``` -If the `Query.feed` field does not have an argument like `type` that you can use in `keyArgs: [...]`, then it may make sense to use the `@connection` directive after all: +If the `Query.feed` field does not have an argument like `category` that you can use in `keyArgs: [...]`, then it might make sense to use the `@connection` directive after all: + ```js const FEED_QUERY = gql` query Feed($offset: Int, $limit: Int, $feedKey: String) { @@ -137,9 +383,11 @@ const FEED_QUERY = gql` } `; ``` -If you execute this query with different values for the `$feedKey` variable, those feed results will be stored separately in the cache, whereas normally they would all be stored in the same list. -When choosing a `keyArgs` configuration for this `Query.feed` field, you should include the `@connection` directive as if it was an argument (the `@` tells `InMemoryCache` you mean a directive): +If you execute this query with different values for the `$feedKey` variable, those feed results are stored separately in the cache, whereas normally they would all be stored in the same list. + +When choosing a `keyArgs` configuration for this `Query.feed` field, you should include the `@connection` directive as if it were an argument (the `@` tells `InMemoryCache` you mean a directive): + ```js const cache = new InMemoryCache({ typePolicies: { @@ -154,7 +402,8 @@ const cache = new InMemoryCache({ }) ``` -With this configuration, your cache will use a `feed:{"@connection":{"key":...}}` key rather than just `feed` to store separate `{ edges, pageInfo }` objects within the `ROOT_QUERY` object: +With this configuration, your cache uses a `feed:{"@connection":{"key":...}}` key instead of just `feed` to store separate `{ edges, pageInfo }` objects within the `ROOT_QUERY` object: + ```js expect(cache.extract()).toEqual({ ROOT_QUERY: { @@ -167,7 +416,8 @@ expect(cache.extract()).toEqual({ }) ``` -The `["key"]` in `keyArgs: ["@connection", ["key"]]` means only the `key` argument to the `@connection` directive will be considered, and any other arguments (like `filter`) will be ignored. Passing just `key` to `@connection` is usually adequate, but if you are tempted to pass a `filter: ["someArg", "anotherArg"]` argument as well, you should instead include those argument names directly in `keyArgs`: +The `["key"]` in `keyArgs: ["@connection", ["key"]]` means only the `key` argument to the `@connection` directive is considered, and any other arguments (like `filter`) are ignored. Passing just `key` to `@connection` is usually adequate, but if you want to pass a `filter: ["someArg", "anotherArg"]` argument as well, you should instead include those argument names directly in `keyArgs`: + ```js const cache = new InMemoryCache({ typePolicies: { @@ -181,11 +431,13 @@ const cache = new InMemoryCache({ }, }) ``` -If any of these arguments or directives are not provided for the current query, they will be omitted from the field key automatically, without error. This means it is generally safe to include more arguments or directives in `keyArgs` than you expect to receive in all cases. -> As mentioned above, if a `keyArgs` array is insufficient to specify your desired field keys, you can alternatively pass a function for `keyArgs`, which takes the `args` object and a `{ typename, field, fieldName, variables }` context parameter, and can either return a string or return a dynamically-generated `keyArgs` array. +If any of these arguments or directives are not provided for the current query, they're omitted from the field key automatically, without error. This means it's generally safe to include more arguments or directives in `keyArgs` than you expect to receive in all cases. + +> As mentioned above, if a `keyArgs` array is insufficient to specify your desired field keys, you can alternatively pass a function for `keyArgs`, which takes the `args` object and a `{ typename, field, fieldName, variables }` context parameter. This function can return either a string or a dynamically-generated `keyArgs` array. Although `keyArgs` (and `@connection`) are useful for more than just paginated fields, it's worth noting that `relayStylePagination` configures `keyArgs: false` by default. You can reconfigure this `keyArgs` behavior by passing an alternate value to `relayStylePagination`: + ```js const cache = new InMemoryCache({ typePolicies: { @@ -197,4 +449,5 @@ const cache = new InMemoryCache({ }, }) ``` + In the unlikely event that a `keyArgs` array is insufficient to capture the identity of a field, remember that you can pass a function for `keyArgs`, which allows you to serialize the `args` object however you want. diff --git a/package-lock.json b/package-lock.json index 2b82576372d..01329fefad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ }, "devDependencies": { "@babel/parser": "7.17.3", - "@graphql-tools/schema": "8.3.1", + "@graphql-tools/schema": "8.3.2", "@rollup/plugin-node-resolve": "11.2.1", "@testing-library/react": "12.1.3", "@testing-library/react-hooks": "7.0.2", @@ -32,9 +32,9 @@ "@types/fetch-mock": "7.3.5", "@types/glob": "7.2.0", "@types/hoist-non-react-statics": "3.3.1", - "@types/jest": "27.4.0", - "@types/lodash": "4.14.178", - "@types/node": "16.11.25", + "@types/jest": "27.4.1", + "@types/lodash": "4.14.179", + "@types/node": "16.11.26", "@types/react": "17.0.34", "@types/react-dom": "17.0.2", "bundlesize": "0.18.1", @@ -53,13 +53,13 @@ "recast": "0.21.0", "resolve": "1.22.0", "rimraf": "3.0.2", - "rollup": "2.67.3", + "rollup": "2.69.2", "rollup-plugin-terser": "7.0.2", "rxjs": "6.6.7", "subscriptions-transport-ws": "0.11.0", - "terser": "5.10.0", + "terser": "5.12.0", "ts-jest": "27.1.3", - "ts-node": "10.5.0", + "ts-node": "10.7.0", "typescript": "4.5.5", "wait-for-observables": "1.0.3", "whatwg-fetch": "3.6.2" @@ -660,38 +660,38 @@ "node": ">=12" } }, - "node_modules/@graphql-tools/schema": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.1.tgz", - "integrity": "sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ==", + "node_modules/@graphql-tools/merge": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.3.tgz", + "integrity": "sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw==", "dev": true, "dependencies": { - "@graphql-tools/merge": "^8.2.1", - "@graphql-tools/utils": "^8.5.1", - "tslib": "~2.3.0", - "value-or-promise": "1.0.11" + "@graphql-tools/utils": "^8.6.2", + "tslib": "~2.3.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/schema/node_modules/@graphql-tools/merge": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.1.tgz", - "integrity": "sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA==", + "node_modules/@graphql-tools/schema": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.2.tgz", + "integrity": "sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg==", "dev": true, "dependencies": { - "@graphql-tools/utils": "^8.5.1", - "tslib": "~2.3.0" + "@graphql-tools/merge": "^8.2.3", + "@graphql-tools/utils": "^8.6.2", + "tslib": "~2.3.0", + "value-or-promise": "1.0.11" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-tools/schema/node_modules/@graphql-tools/utils": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.1.tgz", - "integrity": "sha512-V/OQVpj+Z05qW9ZdlJWSKzREYlgGEq+juV+pUy3JO9jI+sZo/W3oncuW9+1awwp/RkL0aZ9RgjL+XYOgCsmOLw==", + "node_modules/@graphql-tools/utils": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz", + "integrity": "sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA==", "dev": true, "dependencies": { "tslib": "~2.3.0" @@ -1276,19 +1276,19 @@ } }, "node_modules/@types/jest": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz", - "integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==", + "version": "27.4.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", + "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", "dev": true, "dependencies": { - "jest-diff": "^27.0.0", + "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "node_modules/@types/lodash": { - "version": "4.14.178", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", - "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "version": "4.14.179", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz", + "integrity": "sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==", "dev": true }, "node_modules/@types/minimatch": { @@ -1298,9 +1298,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.25.tgz", - "integrity": "sha512-NrTwfD7L1RTc2qrHQD4RTTy4p0CO2LatKBEKEds3CaVuhoM/+DJzmWZl5f+ikR8cm8F5mfJxK+9rQq07gRiSjQ==", + "version": "16.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", + "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==", "dev": true }, "node_modules/@types/prettier": { @@ -5393,9 +5393,9 @@ } }, "node_modules/rollup": { - "version": "2.67.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.67.3.tgz", - "integrity": "sha512-G/x1vUwbGtP6O5ZM8/sWr8+p7YfZhI18pPqMRtMYMWSbHjKZ/ajHGiM+GWNTlWyOR0EHIdT8LHU+Z4ciIZ1oBw==", + "version": "2.69.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.69.2.tgz", + "integrity": "sha512-KghktpWg3Wd+nYCsx3Griidm2/CKIJYG2yyaaKspo0TXSoGdW+0duwzKl4wWIu62oN3mFg3zCDbwVRPwuNPPlA==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -5862,11 +5862,12 @@ } }, "node_modules/terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.0.tgz", + "integrity": "sha512-R3AUhNBGWiFc77HXag+1fXpAxTAFRQTJemlJKjAgD9r8xXTpjNKqIXwHM/o7Rh+O0kUJtS3WQVdBeMKFk5sw9A==", "dev": true, "dependencies": { + "acorn": "^8.5.0", "commander": "^2.20.0", "source-map": "~0.7.2", "source-map-support": "~0.5.20" @@ -5876,14 +5877,6 @@ }, "engines": { "node": ">=10" - }, - "peerDependencies": { - "acorn": "^8.5.0" - }, - "peerDependenciesMeta": { - "acorn": { - "optional": true - } } }, "node_modules/terser/node_modules/source-map": { @@ -6036,9 +6029,9 @@ } }, "node_modules/ts-node": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.5.0.tgz", - "integrity": "sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "0.7.0", @@ -6058,6 +6051,7 @@ "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" @@ -7017,37 +7011,35 @@ "@cspotcode/source-map-consumer": "0.8.0" } }, + "@graphql-tools/merge": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.3.tgz", + "integrity": "sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^8.6.2", + "tslib": "~2.3.0" + } + }, "@graphql-tools/schema": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.1.tgz", - "integrity": "sha512-3R0AJFe715p4GwF067G5i0KCr/XIdvSfDLvTLEiTDQ8V/hwbOHEKHKWlEBHGRQwkG5lwFQlW1aOn7VnlPERnWQ==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.2.tgz", + "integrity": "sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg==", "dev": true, "requires": { - "@graphql-tools/merge": "^8.2.1", - "@graphql-tools/utils": "^8.5.1", + "@graphql-tools/merge": "^8.2.3", + "@graphql-tools/utils": "^8.6.2", "tslib": "~2.3.0", "value-or-promise": "1.0.11" - }, - "dependencies": { - "@graphql-tools/merge": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.1.tgz", - "integrity": "sha512-Q240kcUszhXiAYudjuJgNuLgy9CryDP3wp83NOZQezfA6h3ByYKU7xI6DiKrdjyVaGpYN3ppUmdj0uf5GaXzMA==", - "dev": true, - "requires": { - "@graphql-tools/utils": "^8.5.1", - "tslib": "~2.3.0" - } - }, - "@graphql-tools/utils": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.5.1.tgz", - "integrity": "sha512-V/OQVpj+Z05qW9ZdlJWSKzREYlgGEq+juV+pUy3JO9jI+sZo/W3oncuW9+1awwp/RkL0aZ9RgjL+XYOgCsmOLw==", - "dev": true, - "requires": { - "tslib": "~2.3.0" - } - } + } + }, + "@graphql-tools/utils": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz", + "integrity": "sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA==", + "dev": true, + "requires": { + "tslib": "~2.3.0" } }, "@graphql-typed-document-node/core": { @@ -7527,19 +7519,19 @@ } }, "@types/jest": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz", - "integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==", + "version": "27.4.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", + "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", "dev": true, "requires": { - "jest-diff": "^27.0.0", + "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "@types/lodash": { - "version": "4.14.178", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", - "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", + "version": "4.14.179", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.179.tgz", + "integrity": "sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w==", "dev": true }, "@types/minimatch": { @@ -7549,9 +7541,9 @@ "dev": true }, "@types/node": { - "version": "16.11.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.25.tgz", - "integrity": "sha512-NrTwfD7L1RTc2qrHQD4RTTy4p0CO2LatKBEKEds3CaVuhoM/+DJzmWZl5f+ikR8cm8F5mfJxK+9rQq07gRiSjQ==", + "version": "16.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.26.tgz", + "integrity": "sha512-GZ7bu5A6+4DtG7q9GsoHXy3ALcgeIHP4NnL0Vv2wu0uUB/yQex26v0tf6/na1mm0+bS9Uw+0DFex7aaKr2qawQ==", "dev": true }, "@types/prettier": { @@ -10690,9 +10682,9 @@ } }, "rollup": { - "version": "2.67.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.67.3.tgz", - "integrity": "sha512-G/x1vUwbGtP6O5ZM8/sWr8+p7YfZhI18pPqMRtMYMWSbHjKZ/ajHGiM+GWNTlWyOR0EHIdT8LHU+Z4ciIZ1oBw==", + "version": "2.69.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.69.2.tgz", + "integrity": "sha512-KghktpWg3Wd+nYCsx3Griidm2/CKIJYG2yyaaKspo0TXSoGdW+0duwzKl4wWIu62oN3mFg3zCDbwVRPwuNPPlA==", "dev": true, "requires": { "fsevents": "~2.3.2" @@ -11063,11 +11055,12 @@ } }, "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.0.tgz", + "integrity": "sha512-R3AUhNBGWiFc77HXag+1fXpAxTAFRQTJemlJKjAgD9r8xXTpjNKqIXwHM/o7Rh+O0kUJtS3WQVdBeMKFk5sw9A==", "dev": true, "requires": { + "acorn": "^8.5.0", "commander": "^2.20.0", "source-map": "~0.7.2", "source-map-support": "~0.5.20" @@ -11175,9 +11168,9 @@ } }, "ts-node": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.5.0.tgz", - "integrity": "sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.7.0", diff --git a/package.json b/package.json index 20b38643475..b41fdf029b8 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ }, "devDependencies": { "@babel/parser": "7.17.3", - "@graphql-tools/schema": "8.3.1", + "@graphql-tools/schema": "8.3.2", "@rollup/plugin-node-resolve": "11.2.1", "@testing-library/react": "12.1.3", "@testing-library/react-hooks": "7.0.2", @@ -103,9 +103,9 @@ "@types/fetch-mock": "7.3.5", "@types/glob": "7.2.0", "@types/hoist-non-react-statics": "3.3.1", - "@types/jest": "27.4.0", - "@types/lodash": "4.14.178", - "@types/node": "16.11.25", + "@types/jest": "27.4.1", + "@types/lodash": "4.14.179", + "@types/node": "16.11.26", "@types/react": "17.0.34", "@types/react-dom": "17.0.2", "bundlesize": "0.18.1", @@ -124,13 +124,13 @@ "recast": "0.21.0", "resolve": "1.22.0", "rimraf": "3.0.2", - "rollup": "2.67.3", + "rollup": "2.69.2", "rollup-plugin-terser": "7.0.2", "rxjs": "6.6.7", "subscriptions-transport-ws": "0.11.0", - "terser": "5.10.0", + "terser": "5.12.0", "ts-jest": "27.1.3", - "ts-node": "10.5.0", + "ts-node": "10.7.0", "typescript": "4.5.5", "wait-for-observables": "1.0.3", "whatwg-fetch": "3.6.2"