Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reactiveVars persistence options #7148

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
38 changes: 38 additions & 0 deletions docs/source/local-state/reactive-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,44 @@ This code creates a reactive variable with an empty array as its initial value.

> **Important:** The return value of `makeVar` is a _function_ that you call to read or modify your reactive variable's value.

### Persistence options

Optionally you can make a persistent reactive variable:

```js
import { makeVar } from "@apollo/client";
import AsyncStorage from "@react-native-community/async-storage";

const options = {
storage: AsyncStorage,
storageKey: "@cartItems"
};

const [cartItems, restoreCartItems] = makeVar([], options);

// Example of restoring the variables
function App() {
const [ready, setReady] = React.useState(false);

React.useEffect(() => {
const restoreReactiveVars = async () => {
await restoreCartItems()
// restore more variables here
setReady(true)
}
restoreReactiveVars()
}, [])

if (!ready) return <MyAppLoading />

return (
<ApolloProvider client={client}>
...
</ApolloProvider>
)
}
```

## Reading

To read the value of your reactive variable, call the function returned by `makeVar` with zero arguments:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
"maxSize": "24.75 kB"
"maxSize": "24.86 kB"
}
],
"peerDependencies": {
Expand Down
56 changes: 52 additions & 4 deletions src/cache/inmemory/reactiveVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Slot } from "@wry/context";
import { dep } from "optimism";
import { InMemoryCache } from "./inMemoryCache";
import { ApolloCache } from '../../core';
import { isString } from "../../utilities/common/isString"

export interface ReactiveVar<T> {
(newValue?: T): T;
Expand All @@ -27,9 +28,16 @@ function consumeAndIterate<T>(set: Set<T>, callback: (item: T) => any) {
items.forEach(callback);
}

export function makeVar<T>(value: T): ReactiveVar<T> {
const caches = new Set<ApolloCache<any>>();
const listeners = new Set<ReactiveListener<T>>();
// makeVar overload signatures
export function makeVar<T>(value: T): ReactiveVar<T>
export function makeVar<T>(
value: T,
config: PersistenceConfig
): [ReactiveVar<T>, () => Promise<void>]

export function makeVar<T>(value: T, config?: PersistenceConfig) {
const caches = new Set<ApolloCache<any>>()
const listeners = new Set<ReactiveListener<T>>()

const rv: ReactiveVar<T> = function (newValue) {
if (arguments.length > 0) {
Expand All @@ -44,6 +52,15 @@ export function makeVar<T>(value: T): ReactiveVar<T> {
caches.forEach(broadcast);
// Finally, notify any listeners added via rv.onNextChange.
consumeAndIterate(listeners, listener => listener(value));
// Run persistence options
try {
config?.storage.setItem(
config.storageKey,
cleanValueToSetStorage(value)
);
} catch {
// pass
}
}
} else {
// When reading from the variable, obtain the current cache from
Expand All @@ -64,7 +81,22 @@ export function makeVar<T>(value: T): ReactiveVar<T> {
};
};

return rv;
if (!config) return rv;

const restore = async () => {
// Set reactiveVar to previous value from storage,
// if there is no previous value, do nothing.
try {
const previousValue = await config.storage.getItem(config.storageKey)
if (previousValue) {
rv(isString(value) ? previousValue : JSON.parse(previousValue))
PedroBern marked this conversation as resolved.
Show resolved Hide resolved
}
} catch {
// pass
}
}

return [rv, restore]
}

type Broadcastable = ApolloCache<any> & {
Expand All @@ -78,3 +110,19 @@ function broadcast(cache: Broadcastable) {
cache.broadcastWatches();
}
}

// Persistence utils

export interface PersistentStorage {
getItem: (key: string) => Promise<void | any>;
setItem: (key: string, data: string) => Promise<void | any>;
}

export type PersistenceConfig = {
storage: PersistentStorage;
storageKey: string;
}

function cleanValueToSetStorage(value: any): string {
return isString(value) ? value : JSON.stringify(value);
}
11 changes: 11 additions & 0 deletions src/utilities/common/__tests__/isString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { isString } from '../isString';

describe('isString', () => {
it('should identify strings', () => {
const someString = isString("somestring")
const notStrings = [{}, [], 0, undefined,null]

expect(someString).toEqual(true);
notStrings.forEach(f => expect(isString(f)).toEqual(false));
});
});
3 changes: 3 additions & 0 deletions src/utilities/common/isString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isString(value: any) {
return Object.prototype.toString.call(value) === '[object String]'
}