diff --git a/package.json b/package.json
index 92e42caf8..f085ce387 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "easy-peasy",
- "version": "2.1.1",
+ "version": "2.2.0-alpha.2",
"description": "Easy peasy global state for React",
"license": "MIT",
"main": "dist/easy-peasy.cjs.js",
diff --git a/src/__tests__/issues.test.js b/src/__tests__/issues.test.js
new file mode 100644
index 000000000..de5230bd8
--- /dev/null
+++ b/src/__tests__/issues.test.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { render } from 'react-testing-library';
+
+import { action, createStore, useStore, StoreProvider } from '../index';
+
+test('issue#136', async () => {
+ // arrange
+ const store = createStore({
+ items: {
+ a: { id: 'a', name: 'A' },
+ b: { id: 'b', name: 'B' },
+ c: { id: 'c', name: 'C' },
+ },
+ deleteB: action(state => {
+ delete state.items.b;
+ }),
+ });
+
+ const ListItem = ({ id }) => {
+ const { name } = useStore(s => s.items[id], [id]);
+ return name;
+ };
+
+ function App() {
+ const itemIds = useStore(s => Object.keys(s.items));
+ const items = itemIds.map(id => );
+ return
{items}
;
+ }
+
+ const app = (
+
+
+
+ );
+
+ const { rerender, getByTestId } = render(app);
+
+ expect(getByTestId('items').innerHTML).toBe('ABC');
+
+ act(() => {
+ store.dispatch.deleteB();
+ });
+
+ rerender(
+
+
+ ,
+ );
+
+ expect(getByTestId('items').innerHTML).toBe('AC');
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+});
diff --git a/src/hooks.js b/src/hooks.js
index 26c628742..d4272a2d7 100644
--- a/src/hooks.js
+++ b/src/hooks.js
@@ -1,10 +1,16 @@
-import { useState, useEffect, useContext, useRef } from 'react';
+import { useState, useEffect, useContext, useRef, useMemo } from 'react';
import shallowEqual from 'shallowequal';
import EasyPeasyContext from './context';
import { isStateObject } from './lib';
+let idx = 0;
+
export function useStore(mapState, dependencies = []) {
- const store = useContext(EasyPeasyContext);
+ const id = useMemo(() => {
+ idx += 1;
+ return idx;
+ }, []);
+ const { store, addListener, removeListener } = useContext(EasyPeasyContext);
const [state, setState] = useState(mapState(store.getState()));
// As our effect only fires on mount and unmount it won't have the state
// changes visible to it, therefore we use a mutable ref to track this.
@@ -12,9 +18,8 @@ export function useStore(mapState, dependencies = []) {
// Helps avoid firing of events when unsubscribed, i.e. unmounted
const isActive = useRef(true);
useEffect(() => {
- const calculateState = () => {
- const newState = mapState(store.getState());
- isActive.current = true;
+ const handleStoreUpdate = storeState => {
+ const newState = mapState(storeState);
if (
newState === stateRef.current ||
(isStateObject(newState) &&
@@ -29,23 +34,24 @@ export function useStore(mapState, dependencies = []) {
setState(stateRef.current);
}
};
- calculateState();
- const unsubscribe = store.subscribe(calculateState);
+ isActive.current = true;
+ handleStoreUpdate(store.getState());
+ addListener(id, handleStoreUpdate);
return () => {
- unsubscribe();
isActive.current = false;
+ removeListener(id);
};
}, dependencies);
return state;
}
export function useActions(mapActions) {
- const store = useContext(EasyPeasyContext);
+ const { store } = useContext(EasyPeasyContext);
return mapActions(store.dispatch);
}
export function useDispatch() {
- const store = useContext(EasyPeasyContext);
+ const { store } = useContext(EasyPeasyContext);
return store.dispatch;
}
diff --git a/src/provider.js b/src/provider.js
index 294ecd20a..d3f4372a8 100644
--- a/src/provider.js
+++ b/src/provider.js
@@ -1,10 +1,35 @@
-import React from 'react';
+import React, { useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import StoreContext from './context';
-const StoreProvider = ({ children, store }) => (
- {children}
-);
+const createCtx = store => {
+ const callbackMap = {};
+ return {
+ addListener: (id, fn) => {
+ callbackMap[id] = fn;
+ },
+ removeListener: id => {
+ const listener = callbackMap[id];
+ if (listener) {
+ delete callbackMap[id];
+ }
+ },
+ getListeners: () => Object.values(callbackMap),
+ store,
+ };
+};
+
+const StoreProvider = ({ children, store }) => {
+ const ctx = useMemo(() => createCtx(store), [store]);
+ useEffect(() => {
+ const unsubscribe = store.subscribe(() => {
+ const state = store.getState();
+ ctx.getListeners().map(fn => fn(state));
+ });
+ return unsubscribe;
+ });
+ return {children};
+};
StoreProvider.propTypes = {
children: PropTypes.node.isRequired,