-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement useReactiveVar hook for consuming reactive variables in React components. #6867
Changes from all commits
45179b9
2f494c8
141794c
002e199
b9382b0
b874104
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -147,15 +147,14 @@ This `read` function returns the value of our reactive variable whenever `cartIt | |
|
||
Now, let's create a button component that enables the user to add a product to their cart: | ||
|
||
```jsx{8}:title=AddToCartButton.js | ||
```jsx{7}:title=AddToCartButton.js | ||
import { cartItemsVar } from './cache'; | ||
// ... other imports | ||
|
||
export function AddToCartButton({ productId }) { | ||
const cartItems = cartItemsVar(); | ||
return ( | ||
<div class="add-to-cart-button"> | ||
<Button onClick={() => cartItemsVar([...cartItems, productId])}> | ||
<Button onClick={() => cartItemsVar([...cartItemsVar(), productId])}> | ||
Add to Cart | ||
</Button> | ||
</div> | ||
|
@@ -167,8 +166,6 @@ On click, this button updates the value of `cartItemsVar` to append the button's | |
|
||
Here's a `Cart` component that uses the `GET_CART_ITEMS` query and therefore refreshes automatically whenever the value of `cartItemsVar` changes: | ||
|
||
<ExpansionPanel title="Expand example"> | ||
|
||
```jsx:title=Cart.js | ||
export const GET_CART_ITEMS = gql` | ||
query GetCartItems { | ||
|
@@ -199,7 +196,32 @@ export function Cart() { | |
} | ||
``` | ||
|
||
</ExpansionPanel> | ||
Alternatively, you can read directly from a reactive variable using the `useReactiveVar` hook introduced in Apollo Client 3.2.0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @StephenBarlow Is it okay to refer to an Apollo Client version number in the docs like this? |
||
|
||
```jsx:title=Cart.js | ||
import { useReactiveVar } from '@apollo/client'; | ||
|
||
export function Cart() { | ||
const cartItems = useReactiveVar(cartItemsVar); | ||
|
||
return ( | ||
<div class="cart"> | ||
<Header>My Cart</Header> | ||
{cartItems.length === 0 ? ( | ||
<p>No items in your cart</p> | ||
) : ( | ||
<Fragment> | ||
{cartItems.map(productId => ( | ||
<CartItem key={productId} /> | ||
))} | ||
</Fragment> | ||
)} | ||
</div> | ||
); | ||
} | ||
``` | ||
|
||
As in the earlier `useQuery` example, whenever the `cartItemsVar` variable is updated, any currently-mounted `Cart` components will rerender. Calling `cartItemsVar()` without `useReactiveVar` will not capture this dependency, so future variable updates will not rerender the component. Both of these approaches are useful in different situations. | ||
|
||
### Storing local state in the cache | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,25 +3,47 @@ import { dep } from "optimism"; | |
import { InMemoryCache } from "./inMemoryCache"; | ||
import { ApolloCache } from '../../core'; | ||
|
||
export type ReactiveVar<T> = (newValue?: T) => T; | ||
export interface ReactiveVar<T> { | ||
(newValue?: T): T; | ||
onNextChange(listener: ReactiveListener<T>): () => void; | ||
} | ||
|
||
export type ReactiveListener<T> = (value: T) => any; | ||
|
||
const varDep = dep<ReactiveVar<any>>(); | ||
|
||
// Contextual Slot that acquires its value when custom read functions are | ||
// called in Policies#readField. | ||
export const cacheSlot = new Slot<ApolloCache<any>>(); | ||
|
||
// A listener function could in theory cause another listener to be added | ||
// to the set while we're iterating over it, so it's important to commit | ||
// to the original elements of the set before we begin iterating. See | ||
// iterateObserversSafely for another example of this pattern. | ||
function consumeAndIterate<T>(set: Set<T>, callback: (item: T) => any) { | ||
const items: T[] = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You have every reason to expect that to work, but TypeScript complains:
The ugly truth is that |
||
set.forEach(item => items.push(item)); | ||
set.clear(); | ||
items.forEach(callback); | ||
} | ||
|
||
export function makeVar<T>(value: T): ReactiveVar<T> { | ||
const caches = new Set<ApolloCache<any>>(); | ||
const listeners = new Set<ReactiveListener<T>>(); | ||
|
||
return function rv(newValue) { | ||
const rv: ReactiveVar<T> = function (newValue) { | ||
if (arguments.length > 0) { | ||
if (value !== newValue) { | ||
value = newValue!; | ||
// First, invalidate any fields with custom read functions that | ||
// consumed this variable, so query results involving those fields | ||
// will be recomputed the next time we read them. | ||
varDep.dirty(rv); | ||
// Trigger broadcast for any caches that were previously involved | ||
// in reading this variable. | ||
// Next, broadcast changes to any caches that have previously read | ||
// from this variable. | ||
caches.forEach(broadcast); | ||
// Finally, notify any listeners added via rv.onNextChange. | ||
consumeAndIterate(listeners, listener => listener(value)); | ||
} | ||
} else { | ||
// When reading from the variable, obtain the current cache from | ||
|
@@ -34,12 +56,21 @@ export function makeVar<T>(value: T): ReactiveVar<T> { | |
|
||
return value; | ||
}; | ||
|
||
rv.onNextChange = listener => { | ||
listeners.add(listener); | ||
return () => { | ||
listeners.delete(listener); | ||
benjamn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
}; | ||
|
||
return rv; | ||
} | ||
|
||
type Broadcastable = ApolloCache<any> & { | ||
// This method is protected in InMemoryCache, which we are ignoring, but | ||
// we still want some semblance of type safety when we call it. | ||
broadcastWatches: InMemoryCache["broadcastWatches"]; | ||
broadcastWatches?: InMemoryCache["broadcastWatches"]; | ||
}; | ||
|
||
function broadcast(cache: Broadcastable) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was risky because the button could be clicked long after the component was first rendered.