Expose subscribe / unsubscribe functions from Atom
Summary: Allow subscribing to Atom state changes Reviewed By: mweststrate Differential Revision: D28027692 fbshipit-source-id: 24fd7ea16b013c364bbb1d25b30c48bc698db014
This commit is contained in:
committed by
Facebook GitHub Bot
parent
dcc7e06afc
commit
140cf38ffd
71
desktop/flipper-plugin/src/state/__tests__/atom.node.tsx
Normal file
71
desktop/flipper-plugin/src/state/__tests__/atom.node.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
test('can subscribe to atom state changes', () => {
|
||||||
|
const state = createState('abc');
|
||||||
|
let receivedValue: string | undefined;
|
||||||
|
let receivedPrevValue: string | undefined;
|
||||||
|
const unsubscribe = state.subscribe((value, prevValue) => {
|
||||||
|
receivedValue = value;
|
||||||
|
receivedPrevValue = prevValue;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
state.set('def');
|
||||||
|
expect(receivedValue).toBe('def');
|
||||||
|
expect(receivedPrevValue).toBe('abc');
|
||||||
|
state.set('ghi');
|
||||||
|
expect(receivedValue).toBe('ghi');
|
||||||
|
expect(receivedPrevValue).toBe('def');
|
||||||
|
} finally {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can unsubscribe from atom state changes', () => {
|
||||||
|
const state = createState('abc');
|
||||||
|
let receivedValue: string | undefined;
|
||||||
|
let receivedPrevValue: string | undefined;
|
||||||
|
const unsubscribe = state.subscribe((value, prevValue) => {
|
||||||
|
receivedValue = value;
|
||||||
|
receivedPrevValue = prevValue;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
state.set('def');
|
||||||
|
expect(receivedValue).toBe('def');
|
||||||
|
expect(receivedPrevValue).toBe('abc');
|
||||||
|
} finally {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
state.set('ghi');
|
||||||
|
expect(receivedValue).toBe('def');
|
||||||
|
expect(receivedPrevValue).toBe('abc');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can unsubscribe from atom state changes using unsubscribe method', () => {
|
||||||
|
const state = createState('abc');
|
||||||
|
let receivedValue: string | undefined;
|
||||||
|
let receivedPrevValue: string | undefined;
|
||||||
|
const listener = (value: string, prevValue: string) => {
|
||||||
|
receivedValue = value;
|
||||||
|
receivedPrevValue = prevValue;
|
||||||
|
};
|
||||||
|
state.subscribe(listener);
|
||||||
|
try {
|
||||||
|
state.set('def');
|
||||||
|
expect(receivedValue).toBe('def');
|
||||||
|
expect(receivedPrevValue).toBe('abc');
|
||||||
|
} finally {
|
||||||
|
state.unsubscribe(listener);
|
||||||
|
}
|
||||||
|
state.set('ghi');
|
||||||
|
expect(receivedValue).toBe('def');
|
||||||
|
expect(receivedPrevValue).toBe('abc');
|
||||||
|
});
|
||||||
@@ -17,11 +17,13 @@ export type Atom<T> = {
|
|||||||
get(): T;
|
get(): T;
|
||||||
set(newValue: T): void;
|
set(newValue: T): void;
|
||||||
update(recipe: (draft: Draft<T>) => void): void;
|
update(recipe: (draft: Draft<T>) => void): void;
|
||||||
|
subscribe(listener: (value: T, prevValue: T) => void): () => void;
|
||||||
|
unsubscribe(listener: (value: T, prevValue: T) => void): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AtomValue<T> implements Atom<T>, Persistable {
|
class AtomValue<T> implements Atom<T>, Persistable {
|
||||||
value: T;
|
value: T;
|
||||||
listeners: ((value: T) => void)[] = [];
|
listeners: ((value: T, prevValue: T) => void)[] = [];
|
||||||
|
|
||||||
constructor(initialValue: T) {
|
constructor(initialValue: T) {
|
||||||
this.value = initialValue;
|
this.value = initialValue;
|
||||||
@@ -33,8 +35,9 @@ class AtomValue<T> implements Atom<T>, Persistable {
|
|||||||
|
|
||||||
set(nextValue: T) {
|
set(nextValue: T) {
|
||||||
if (nextValue !== this.value) {
|
if (nextValue !== this.value) {
|
||||||
|
const prevValue = this.value;
|
||||||
this.value = nextValue;
|
this.value = nextValue;
|
||||||
this.notifyChanged();
|
this.notifyChanged(prevValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,16 +53,17 @@ class AtomValue<T> implements Atom<T>, Persistable {
|
|||||||
this.set(produce(this.value, recipe));
|
this.set(produce(this.value, recipe));
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyChanged() {
|
notifyChanged(prevValue: T) {
|
||||||
// TODO: add scheduling
|
// TODO: add scheduling
|
||||||
this.listeners.slice().forEach((l) => l(this.value));
|
this.listeners.slice().forEach((l) => l(this.value, prevValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(listener: (value: T) => void) {
|
subscribe(listener: (value: T, prevValue: T) => void) {
|
||||||
this.listeners.push(listener);
|
this.listeners.push(listener);
|
||||||
|
return () => this.unsubscribe(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe(listener: (value: T) => void) {
|
unsubscribe(listener: (value: T, prevValue: T) => void) {
|
||||||
const idx = this.listeners.indexOf(listener);
|
const idx = this.listeners.indexOf(listener);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
this.listeners.splice(idx, 1);
|
this.listeners.splice(idx, 1);
|
||||||
|
|||||||
@@ -463,6 +463,8 @@ A state atom object is returned by `createState`, exposing the following methods
|
|||||||
* `get(): T`: Returns the current value stored. If you want to use the atom object in a React component, consider using the `useValue` hook instead, to make sure the component is notified about future updates of this atom.
|
* `get(): T`: Returns the current value stored. If you want to use the atom object in a React component, consider using the `useValue` hook instead, to make sure the component is notified about future updates of this atom.
|
||||||
* `set(newValue: T)`: Stores a new value into the atom. If the new value is not reference-equal to the previous one, all observing components will be notified.
|
* `set(newValue: T)`: Stores a new value into the atom. If the new value is not reference-equal to the previous one, all observing components will be notified.
|
||||||
* `update(updater: (draft: Draft<T>) => void)`: Updates the current state using an [Immer](https://immerjs.github.io/immer/docs/introduction) recipe. In the `updater`, the `draft` object can be safely (deeply) mutated. Once the `updater` finishes, Immer will compute a new immutable object based on the changes, and store that. This is often simpler than using a combination of `get` and `set` if deep updates need to be made to the stored object.
|
* `update(updater: (draft: Draft<T>) => void)`: Updates the current state using an [Immer](https://immerjs.github.io/immer/docs/introduction) recipe. In the `updater`, the `draft` object can be safely (deeply) mutated. Once the `updater` finishes, Immer will compute a new immutable object based on the changes, and store that. This is often simpler than using a combination of `get` and `set` if deep updates need to be made to the stored object.
|
||||||
|
* `subscribe(listener: (value: T, prevValue: T) => void): () => void`: Subscribes a listener function to the state updates. Listener function will receive the next and previous value on each update. The method also returns function which can be called to unsubscribe the listener from further updates.
|
||||||
|
* `unsubscribe(listener: (value: T, prevValue: T) => void): void`: Unsubscribes a listener function from the state updates if it was subscribed before.
|
||||||
|
|
||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
@@ -472,9 +474,14 @@ import {createState} from 'flipper-plugin'
|
|||||||
const rows = createState<string[]>([], {persist: 'rows'});
|
const rows = createState<string[]>([], {persist: 'rows'});
|
||||||
const selectedID = createState<string | null>(null, {persist: 'selection'});
|
const selectedID = createState<string | null>(null, {persist: 'selection'});
|
||||||
|
|
||||||
rows.set(["hello"])
|
// Listener will be called on each rows.set() and rows.update() call until unsubscribed.
|
||||||
|
const unsubscribe = rows.subscribe((value, prevValue) => {
|
||||||
|
console.log(`Rows state updated. New length: ${value.length}. Prev length: ${prevValue.length}.`);
|
||||||
|
});
|
||||||
|
rows.set(["hello"]) // Listener will be notified about the change
|
||||||
console.log(rows.get().length) // 1
|
console.log(rows.get().length) // 1
|
||||||
rows.update(draft => {
|
unsubscribe(); // Do not notify listener anymore
|
||||||
|
rows.update(draft => { // Listener won't be notified about the change
|
||||||
draft.push("world")
|
draft.push("world")
|
||||||
})
|
})
|
||||||
console.log(rows.get().length) // 2
|
console.log(rows.get().length) // 2
|
||||||
|
|||||||
Reference in New Issue
Block a user