Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
54 changes: 54 additions & 0 deletions src/__tests__/issues.test.js
Original file line number Diff line number Diff line change
@@ -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 => <ListItem key={id} id={id} />);
return <div data-testid="items">{items}</div>;
}

const app = (
<StoreProvider store={store}>
<App />
</StoreProvider>
);

const { rerender, getByTestId } = render(app);

expect(getByTestId('items').innerHTML).toBe('ABC');

act(() => {
store.dispatch.deleteB();
});

rerender(
<StoreProvider store={store}>
<App />
</StoreProvider>,
);

expect(getByTestId('items').innerHTML).toBe('AC');

await new Promise(resolve => setTimeout(resolve, 1000));
});
26 changes: 16 additions & 10 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
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.
const stateRef = useRef(state);
// 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) &&
Expand All @@ -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;
}

Expand Down
33 changes: 29 additions & 4 deletions src/provider.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
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 <StoreContext.Provider value={ctx}>{children}</StoreContext.Provider>;
};

StoreProvider.propTypes = {
children: PropTypes.node.isRequired,
Expand Down