Introduce localStorage support to createState

Summary:
Per title.

Feature will be used in several plugins in next diffs.

Differential Revision: D29514456

fbshipit-source-id: c12427c2a7c53fa01cd1c7f429be8611be55496d
This commit is contained in:
Michel Weststrate
2021-07-01 07:19:26 -07:00
committed by Facebook GitHub Bot
parent dfd895cec0
commit 25373a3089
3 changed files with 128 additions and 6 deletions

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {createState} from '../atom';
import * as TestUtils from '../../test-utils/test-utils';
beforeEach(() => {
window.localStorage.clear();
});
test('it can start a plugin and lifecycle events', () => {
window.localStorage.setItem('flipper:TestPlugin:atom:x', '{ "x": 2 }');
const testPlugin = {
plugin() {
const x = createState<{x: number}>(
{x: 1},
{
persist: 'x',
persistToLocalStorage: true,
},
);
const y = createState(true, {
persist: 'y',
persistToLocalStorage: true,
});
return {x, y};
},
Component() {
return null;
},
};
const {instance} = TestUtils.startPlugin(testPlugin);
expect(instance.x.get()).toEqual({x: 2});
expect(instance.y.get()).toEqual(true);
expect(getStorageSnapshot()).toMatchInlineSnapshot(`
Object {
"flipper:TestPlugin:atom:x": "{ \\"x\\": 2 }",
}
`);
instance.x.update((d) => {
d.x++;
});
instance.y.set(false);
expect(getStorageSnapshot()).toMatchInlineSnapshot(`
Object {
"flipper:TestPlugin:atom:x": "{\\"x\\":3}",
"flipper:TestPlugin:atom:y": "false",
}
`);
});
function getStorageSnapshot() {
const res: Record<string, string> = {};
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)!;
res[key] = window.localStorage.getItem(key)!;
}
return res;
}
test('localStorage requires persist key', () => {
expect(() =>
createState(3, {persistToLocalStorage: true}),
).toThrowErrorMatchingInlineSnapshot(
`"The 'persist' option should be set when 'persistToLocalStorage' is set"`,
);
});
test('localStorage requires plugin context', () => {
expect(() =>
createState(3, {persistToLocalStorage: true, persist: 'x'}),
).toThrowErrorMatchingInlineSnapshot(
`"The 'persistToLocalStorage' option cannot be used outside a plugin definition"`,
);
});

View File

@@ -9,7 +9,11 @@
import {produce, Draft, enableMapSet} from 'immer';
import {useState, useEffect} from 'react';
import {Persistable, registerStorageAtom} from '../plugin/PluginBase';
import {
getCurrentPluginInstance,
Persistable,
registerStorageAtom,
} from '../plugin/PluginBase';
import {
deserializeShallowObject,
makeShallowSerializable,
@@ -85,6 +89,12 @@ type StateOptions = {
* If set, the atom will be saved / loaded under the key provided
*/
persist?: string;
/**
* Store this state in local storage, instead of as part of the plugin import / export.
* State stored in local storage is shared between the same plugin
* across multiple clients/ devices, but not actively synced.
*/
persistToLocalStorage?: boolean;
};
export function createState<T>(
@@ -97,10 +107,36 @@ export function createState(
options: StateOptions = {},
): Atom<any> {
const atom = new AtomValue(initialValue);
registerStorageAtom(options.persist, atom);
if (options?.persistToLocalStorage) {
syncAtomWithLocalStorage(options, atom);
} else {
registerStorageAtom(options.persist, atom);
}
return atom;
}
function syncAtomWithLocalStorage(options: StateOptions, atom: AtomValue<any>) {
if (!options?.persist) {
throw new Error(
"The 'persist' option should be set when 'persistToLocalStorage' is set",
);
}
const pluginInstance = getCurrentPluginInstance();
if (!pluginInstance) {
throw new Error(
"The 'persistToLocalStorage' option cannot be used outside a plugin definition",
);
}
const storageKey = `flipper:${pluginInstance.definition.id}:atom:${options.persist}`;
const storedValue = window.localStorage.getItem(storageKey);
if (storedValue != null) {
atom.deserialize(JSON.parse(storedValue));
}
atom.subscribe(() => {
window.localStorage.setItem(storageKey, JSON.stringify(atom.serialize()));
});
}
export function useValue<T>(atom: ReadOnlyAtom<T>): T;
export function useValue<T>(
atom: ReadOnlyAtom<T> | undefined,