Commit c313ec3b authored by Robert Knight's avatar Robert Knight

Add utility to watch for changes in certain values

This will be used to streamline a common pattern in our code for
reacting to changes in state selected from the central Redux store.

The utility is a standalone function rather than a method of the store
so that it can easily be used with mock stores and also other data
sources in future.

```
watch(
  store.subscribe,
  () => store.getState().someValue,
  (currentValue, prevValue) => { /* Handle change */ },
);
```
parent b802467d
import { createStore } from 'redux';
import { watch } from '../watch';
function counterReducer(state = { a: 0, b: 0 }, action) {
switch (action.type) {
case 'increment-a':
return { ...state, a: state.a + 1 };
case 'increment-b':
return { ...state, b: state.b + 1 };
default:
return state;
}
}
describe('sidebar/util/watch', () => {
/**
* Create a Redux store as a data source for testing the `watch` function.
*/
function counterStore() {
return createStore(counterReducer);
}
describe('watch', () => {
it('runs callback when computed value changes', () => {
const callback = sinon.stub();
const store = counterStore();
watch(store.subscribe, () => store.getState().a, callback);
store.dispatch({ type: 'increment-a' });
assert.calledWith(callback, 1, 0);
store.dispatch({ type: 'increment-a' });
assert.calledWith(callback, 2, 1);
});
it('does not run callback if computed value did not change', () => {
const callback = sinon.stub();
const store = counterStore();
watch(store.subscribe, () => store.getState().a, callback);
store.dispatch({ type: 'increment-b' });
assert.notCalled(callback);
});
it('supports multiple value functions', () => {
const callback = sinon.stub();
const store = counterStore();
watch(
store.subscribe,
[() => store.getState().a, () => store.getState().b],
callback
);
store.dispatch({ type: 'increment-a' });
assert.calledWith(callback, [1, 0], [0, 0]);
});
it('returns unsubscription function', () => {
const callback = sinon.stub();
const store = counterStore();
const unsubscribe = watch(
store.subscribe,
() => store.getState().a,
callback
);
store.dispatch({ type: 'increment-a' });
assert.calledWith(callback, 1, 0);
callback.resetHistory();
unsubscribe();
store.dispatch({ type: 'increment-a' });
assert.notCalled(callback);
});
});
});
import shallowEqual from 'shallowequal';
/**
* Watch for changes of computed values.
*
* This utility is a shorthand for a common pattern for reacting to changes in
* some data source:
*
* ```
* let prevValue = getCurrentValue();
* subscribe(() => {
* const newValue = getCurrentValue();
* if (prevValue !== newValue) {
* // Respond to change of value.
* // ...
*
* // Update previous value.
* prevValue = new value;
* }
* });
* ```
*
* Where `getCurrentValue` calculates the value of interest and
* `subscribe` registers a callback to receive change notifications for
* whatever data source (eg. a Redux store) is used by `getCurrentValue`.
*
* With the `watch` utility this becomes:
*
* ```
* watch(subscribe, getCurrentValue, (newValue, prevValue) => {
* // Respond to change of value
* });
* ```
*
* `watch` can watch a single value, if the second argument is a function,
* or many if the second argument is an array of functions. In the latter case
* the callback will be invoked whenever _any_ of the watched values changes.
*
* Values are compared using strict equality (`===`).
*
* @param {(callback: Function) => Function} subscribe - Function used to
* subscribe to notifications of _potential_ changes in the watched values.
* @param {Function|Array<Function>} watchFns - A function or array of functions
* which return the current watched values
* @param {(current: any, previous: any) => any} callback -
* A callback that is invoked when the watched values changed. It is passed
* the current and previous values respectively. If `watchFns` is an array,
* the `current` and `previous` arguments will be arrays of current and
* previous values.
* @return {Function} - Return value of `subscribe`. Typically this is a
* function that removes the subscription.
*/
export function watch(subscribe, watchFns, callback) {
const getWatchedValues = () =>
Array.isArray(watchFns) ? watchFns.map(fn => fn()) : watchFns();
let prevValues = getWatchedValues();
const unsubscribe = subscribe(() => {
const values = getWatchedValues();
if (shallowEqual(values, prevValues)) {
return;
}
// Save and then update `prevValues` before invoking `callback` in case
// `callback` triggers another update.
const savedPrevValues = prevValues;
prevValues = values;
callback(values, savedPrevValues);
});
return unsubscribe;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment