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:
Anton Nikolaev
2021-04-27 09:30:11 -07:00
committed by Facebook GitHub Bot
parent dcc7e06afc
commit 140cf38ffd
3 changed files with 90 additions and 8 deletions

View 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');
});

View File

@@ -17,11 +17,13 @@ export type Atom<T> = {
get(): T;
set(newValue: T): 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 {
value: T;
listeners: ((value: T) => void)[] = [];
listeners: ((value: T, prevValue: T) => void)[] = [];
constructor(initialValue: T) {
this.value = initialValue;
@@ -33,8 +35,9 @@ class AtomValue<T> implements Atom<T>, Persistable {
set(nextValue: T) {
if (nextValue !== this.value) {
const prevValue = this.value;
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));
}
notifyChanged() {
notifyChanged(prevValue: T) {
// 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);
return () => this.unsubscribe(listener);
}
unsubscribe(listener: (value: T) => void) {
unsubscribe(listener: (value: T, prevValue: T) => void) {
const idx = this.listeners.indexOf(listener);
if (idx !== -1) {
this.listeners.splice(idx, 1);

View File

@@ -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.
* `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.
* `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
@@ -472,9 +474,14 @@ import {createState} from 'flipper-plugin'
const rows = createState<string[]>([], {persist: 'rows'});
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
rows.update(draft => {
unsubscribe(); // Do not notify listener anymore
rows.update(draft => { // Listener won't be notified about the change
draft.push("world")
})
console.log(rows.get().length) // 2