Commit d9e99916 authored by Robert Knight's avatar Robert Knight

Remove old `useStore` hook

All consumers now use the replacement `useStoreProxy` hook.
parent d2e9f195
import { mount } from 'enzyme';
import { createElement } from 'preact';
import { act } from 'preact/test-utils';
import { createStore as createReduxStore } from 'redux';
import createStore from '../create-store';
import useStore, { useStoreProxy, $imports } from '../use-store';
// Plain Redux reducer used by `useStore` tests. Remove once `useStore` is removed.
const initialState = { value: 10, otherValue: 20 };
const reducer = (state = initialState, action) => {
if (action.type === 'INCREMENT') {
return { ...state, value: state.value + 1 };
} else if (action.type === 'INCREMENT_OTHER') {
return { ...state, otherValue: state.otherValue + 1 };
} else {
return state;
}
};
import { useStoreProxy, $imports } from '../use-store';
// Store module for use with `createStore` in tests.
const thingsModule = {
......@@ -57,91 +44,6 @@ describe('sidebar/store/use-store', () => {
$imports.$restore();
});
// Tests for deprecated `useStore` function.
describe('useStore', () => {
let renderCount;
let testStore;
let TestComponent;
beforeEach(() => {
renderCount = 0;
// eslint-disable-next-line react/display-name
TestComponent = () => {
renderCount += 1;
const aValue = useStore(store => store.getState().value);
return <div>{aValue}</div>;
};
testStore = createReduxStore(reducer);
$imports.$mock({
'../util/service-context': {
useService: name => (name === 'store' ? testStore : null),
},
});
});
it('returns result of `callback(store)`', () => {
const wrapper = mount(<TestComponent />);
assert.equal(wrapper.text(), '10');
});
it('re-renders when the store changes and result of `callback(store)` also changes', () => {
// An update which changes result of `callback(store)` should cause a re-render.
const wrapper = mount(<TestComponent />);
act(() => {
testStore.dispatch({ type: 'INCREMENT' });
});
wrapper.update();
assert.equal(wrapper.text(), '11');
// The new result from `callback(store)` should be remembered so that another
// update which doesn't change the result doesn't cause a re-render.
const prevRenderCount = renderCount;
act(() => {
testStore.dispatch({ type: 'INCREMENT_OTHER' });
});
wrapper.update();
assert.equal(renderCount, prevRenderCount);
});
it('does not re-render if the result of `callback(store)` did not change', () => {
mount(<TestComponent />);
const originalRenderCount = renderCount;
act(() => {
testStore.dispatch({ type: 'INCREMENT_OTHER' });
});
assert.equal(renderCount, originalRenderCount);
});
it('warns if the callback always returns a different value', () => {
const warnOnce = sinon.stub();
$imports.$mock({
'../../shared/warn-once': warnOnce,
});
const BuggyComponent = () => {
// The result of the callback is an object with an `aValue` property
// which is a new array every time. This causes unnecessary re-renders.
useStore(() => ({ aValue: [] }));
return null;
};
mount(<BuggyComponent />);
assert.called(warnOnce);
assert.match(warnOnce.firstCall.args[0], /changes every time/);
});
it('unsubscribes when the component is unmounted', () => {
const unsubscribe = sinon.stub();
testStore.subscribe = sinon.stub().returns(unsubscribe);
const wrapper = mount(<TestComponent />);
assert.calledOnce(testStore.subscribe);
wrapper.unmount();
assert.calledOnce(unsubscribe);
});
});
describe('useStoreProxy', () => {
let store;
let renderCount;
......
/* global process */
import { useEffect, useRef, useReducer } from 'preact/hooks';
import shallowEqual from 'shallowequal';
import warnOnce from '../../shared/warn-once';
import { useService } from '../util/service-context';
/** @typedef {import("redux").Store} Store */
/** @typedef {import("./index").SidebarStore} SidebarStore */
/**
* @template T
* @callback StoreCallback
* @param {SidebarStore} store
* @return {T}
*/
/**
* Hook for accessing state or actions from the store inside a component.
*
* This hook fetches the store using `useService` and returns the result of
* passing it to the provided callback. The callback will be re-run whenever
* the store updates and the component will be re-rendered if the result of
* `callback(store)` changed.
*
* This ensures that the component updates when relevant store state changes.
*
* @example
* function MyWidget({ widgetId }) {
* const widget = useStore(store => store.getWidget(widgetId));
* const hideWidget = useStore(store => store.hideWidget);
*
* return (
* <div>
* {widget.name}
* <button onClick={() => hideWidget(widgetId)}>Hide</button>
* </div>
* )
* }
*
* @template T
* @param {StoreCallback<T>} callback -
* Callback that receives the store as an argument and returns some state
* and/or actions extracted from the store.
* @return {T} - The result of `callback(store)`
*/
export default function useStore(callback) {
const store = useService('store');
// Store the last-used callback in a ref so we can access it in the effect
// below without having to re-subscribe to the store when it changes.
const lastCallback = useRef(/** @type {StoreCallback<T>|null} */ (null));
lastCallback.current = callback;
const lastResult = useRef(/** @type {T|undefined} */ (undefined));
lastResult.current = callback(store);
// Check for a performance issue caused by `callback` returning a different
// result on every call, even if the store has not changed.
if (process.env.NODE_ENV !== 'production') {
if (!shallowEqual(lastResult.current, callback(store))) {
warnOnce(
'The output of a callback passed to `useStore` changes every time. ' +
'This will lead to a component updating more often than necessary.'
);
}
}
// Abuse `useReducer` to force updates when the store state changes.
const [, forceUpdate] = useReducer(x => x + 1, 0);
// Connect to the store, call `callback(store)` whenever the store changes
// and re-render the component if the result changed.
useEffect(() => {
function checkForUpdate() {
const result = lastCallback.current(store);
if (shallowEqual(result, lastResult.current)) {
return;
}
lastResult.current = result;
// Force this function to ignore parameters and just force a store update.
/** @type {()=>any} */ (forceUpdate)();
}
// Check for any changes since the component was rendered.
checkForUpdate();
// Check for updates when the store changes in future.
const unsubscribe = store.subscribe(checkForUpdate);
// Remove the subscription when the component is unmounted.
return unsubscribe;
}, [forceUpdate, store]);
return lastResult.current;
}
/**
* Result of a cached store selector method call.
*/
......
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