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:
committed by
Facebook GitHub Bot
parent
dfd895cec0
commit
25373a3089
@@ -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"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -9,7 +9,11 @@
|
|||||||
|
|
||||||
import {produce, Draft, enableMapSet} from 'immer';
|
import {produce, Draft, enableMapSet} from 'immer';
|
||||||
import {useState, useEffect} from 'react';
|
import {useState, useEffect} from 'react';
|
||||||
import {Persistable, registerStorageAtom} from '../plugin/PluginBase';
|
import {
|
||||||
|
getCurrentPluginInstance,
|
||||||
|
Persistable,
|
||||||
|
registerStorageAtom,
|
||||||
|
} from '../plugin/PluginBase';
|
||||||
import {
|
import {
|
||||||
deserializeShallowObject,
|
deserializeShallowObject,
|
||||||
makeShallowSerializable,
|
makeShallowSerializable,
|
||||||
@@ -85,6 +89,12 @@ type StateOptions = {
|
|||||||
* If set, the atom will be saved / loaded under the key provided
|
* If set, the atom will be saved / loaded under the key provided
|
||||||
*/
|
*/
|
||||||
persist?: string;
|
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>(
|
export function createState<T>(
|
||||||
@@ -97,10 +107,36 @@ export function createState(
|
|||||||
options: StateOptions = {},
|
options: StateOptions = {},
|
||||||
): Atom<any> {
|
): Atom<any> {
|
||||||
const atom = new AtomValue(initialValue);
|
const atom = new AtomValue(initialValue);
|
||||||
registerStorageAtom(options.persist, atom);
|
if (options?.persistToLocalStorage) {
|
||||||
|
syncAtomWithLocalStorage(options, atom);
|
||||||
|
} else {
|
||||||
|
registerStorageAtom(options.persist, atom);
|
||||||
|
}
|
||||||
return 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>): T;
|
||||||
export function useValue<T>(
|
export function useValue<T>(
|
||||||
atom: ReadOnlyAtom<T> | undefined,
|
atom: ReadOnlyAtom<T> | undefined,
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ Its value should be treated as immutable and is initialized by default using the
|
|||||||
Optionally, `options` can be provided when creating state. Supported options:
|
Optionally, `options` can be provided when creating state. Supported options:
|
||||||
|
|
||||||
* `persist: string`. If the `persist` value is set, this state container will be serialized when n Flipper snapshot export is being made. When a snapshot is imported into Flipper, and plugins are initialized, this state container will load its initial value from the snapshot, rather than using the `initialValue` parameter. The `persist` key should be unique within the plugin and only be set if the state stored in this container is serializable and won't become unreasonably large. See also `exportState` and `initialState` in the [`TestUtils`](#testutils) section.
|
* `persist: string`. If the `persist` value is set, this state container will be serialized when n Flipper snapshot export is being made. When a snapshot is imported into Flipper, and plugins are initialized, this state container will load its initial value from the snapshot, rather than using the `initialValue` parameter. The `persist` key should be unique within the plugin and only be set if the state stored in this container is serializable and won't become unreasonably large. See also `exportState` and `initialState` in the [`TestUtils`](#testutils) section.
|
||||||
|
* `persistToLocalStorage: boolean`. If this option is set in combination with the `persist` option. The atom will store its 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.
|
||||||
|
|
||||||
Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects and arrays. Precisely as the root, `Date`, `Set` or `Map` objects are allowed as well, but they shouldn't appear deeper in the tree.
|
Serializable is defined as: non-cyclic data, consisting purely of primitive values, plain objects and arrays. Precisely as the root, `Date`, `Set` or `Map` objects are allowed as well, but they shouldn't appear deeper in the tree.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user