diff --git a/jest.config.js b/jest.config.js index 6216a3e..597b053 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,5 +8,6 @@ module.exports = { }, collectCoverage: true, + coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], coverageReporters: ['json', 'html'], }; diff --git a/src/SyncReduxToRecoil.tsx b/src/SyncReduxToRecoil.tsx index 3f66d76..f7aa3a5 100644 --- a/src/SyncReduxToRecoil.tsx +++ b/src/SyncReduxToRecoil.tsx @@ -1,13 +1,10 @@ import React from 'react'; -import { Store } from 'redux'; import { useSelector, useStore } from 'react-redux'; import { useRecoilState } from 'recoil'; -import { ReduxState, internalStateAtom } from './internals'; +import { ReduxState, reduxStateAtom, reduxStoreRef } from './internals'; const selectEntireState = (state: ReduxState) => state; -let store: Store | null = null; -const getStore = (): Store | null => store; export interface SyncReduxToRecoilProps { enabled?: boolean; @@ -16,8 +13,8 @@ export interface SyncReduxToRecoilProps { const SyncReduxToRecoil: React.FC = (props) => { const { children, enabled } = props; - store = useStore(); - const [lastReduxState, setReduxState] = useRecoilState(internalStateAtom); + reduxStoreRef.c = useStore(); + const [lastReduxState, setReduxState] = useRecoilState(reduxStateAtom); const currentReduxState = useSelector(selectEntireState); if (enabled && currentReduxState !== lastReduxState) { @@ -38,4 +35,3 @@ SyncReduxToRecoil.defaultProps = { }; export default SyncReduxToRecoil; -export { getStore }; diff --git a/src/atomFromRedux.ts b/src/atomFromRedux.ts index 0744b80..2272103 100644 --- a/src/atomFromRedux.ts +++ b/src/atomFromRedux.ts @@ -4,11 +4,11 @@ import { RecoilState, selectorFamily } from 'recoil'; import { ChangeEntry, DefaultReturnType, - applyChangesToState, - internalStateAtom, + applyChangesToObject, + reduxStateAtom, + reduxStoreRef, syncChangesFromRecoilAction, } from './internals'; -import { getStore } from './SyncReduxToRecoil'; const atomSelectorCache = Object.create(null); const { hasOwnProperty } = Object.prototype; @@ -16,25 +16,25 @@ const { hasOwnProperty } = Object.prototype; const atomSelectorFamily = selectorFamily({ key: 'redux-to-recoil:atom', get: (realPath: string) => ({ get }) => { - const reduxState = get(internalStateAtom); + const reduxState = get(reduxStateAtom); if (realPath) { return getPath(reduxState, realPath); } return reduxState; }, set: (realPath: string) => ({ get, set }, newValue: unknown) => { - const reduxState = get(internalStateAtom); + const reduxState = get(reduxStateAtom); const thisChange: ChangeEntry = [realPath, newValue]; // @TODO: Batching support const allChanges = [thisChange]; - const newState = applyChangesToState(reduxState, allChanges); + const newState = applyChangesToObject(reduxState, allChanges); - set(internalStateAtom, newState); - const reduxStore = getStore(); + set(reduxStateAtom, newState); + const reduxStore = reduxStoreRef.c; if (reduxStore) { reduxStore.dispatch(syncChangesFromRecoilAction(allChanges)); } else if (__DEV__) { - console.error('Cannot dispatch to Redux store because it is not synced'); + throw new Error('Cannot dispatch to Redux store because is not mounted'); } }, }); @@ -47,9 +47,7 @@ const atomFromRedux = (path: string): RecoilStat if (!hasOwnProperty.call(atomSelectorCache, realPath)) { // Although named "atomFromRedux", each instance is actually just a selector. They all pull from a single atom. - const selectorForPath = atomSelectorFamily(realPath); - - atomSelectorCache[realPath] = selectorForPath; + atomSelectorCache[realPath] = atomSelectorFamily(realPath); } return atomSelectorCache[realPath]; diff --git a/src/internals/applyChangesToState.ts b/src/internals/applyChangesToObject.ts similarity index 73% rename from src/internals/applyChangesToState.ts rename to src/internals/applyChangesToObject.ts index 2f351be..7b69c79 100644 --- a/src/internals/applyChangesToState.ts +++ b/src/internals/applyChangesToObject.ts @@ -2,7 +2,7 @@ import { set as setPath } from 'immutable-path/dist/immutable-object-path'; import { ChangeEntry, ReduxState } from './types'; -const applyChangesToState = (state: ReduxState, changes: Array): ReduxState => { +const applyChangesToObject = (state: ReduxState, changes: Array): ReduxState => { let newState = state; for (let i = 0; i < changes.length; i++) { const [path, value] = changes[i]; @@ -16,4 +16,4 @@ const applyChangesToState = (state: ReduxState, changes: Array): Re return newState; }; -export default applyChangesToState; +export default applyChangesToObject; diff --git a/src/internals/index.ts b/src/internals/index.ts index 3362cdd..d5aa74c 100644 --- a/src/internals/index.ts +++ b/src/internals/index.ts @@ -1,8 +1,11 @@ -export { default as applyChangesToState } from './applyChangesToState'; -export * from './applyChangesToState'; +export { default as applyChangesToObject } from './applyChangesToObject'; +export * from './applyChangesToObject'; -export { default as internalStateAtom } from './internalStateAtom'; -export * from './internalStateAtom'; +export { default as reduxStateAtom } from './reduxStateAtom'; +export * from './reduxStateAtom'; + +export { default as reduxStoreRef } from './reduxStoreRef'; +export * from './reduxStoreRef'; export { default as syncChangesFromRecoilAction } from './syncChangesFromRecoilAction'; export * from './syncChangesFromRecoilAction'; diff --git a/src/internals/internalStateAtom.ts b/src/internals/reduxStateAtom.ts similarity index 61% rename from src/internals/internalStateAtom.ts rename to src/internals/reduxStateAtom.ts index 1a1c921..b69071a 100644 --- a/src/internals/internalStateAtom.ts +++ b/src/internals/reduxStateAtom.ts @@ -2,9 +2,9 @@ import { atom } from 'recoil'; import { ReduxState } from './types'; -const internalStateAtom = atom({ +const reduxStateAtom = atom({ key: 'redux-to-recoil:state', default: null, }); -export default internalStateAtom; +export default reduxStateAtom; diff --git a/src/internals/reduxStoreRef.ts b/src/internals/reduxStoreRef.ts new file mode 100644 index 0000000..9a6bb4d --- /dev/null +++ b/src/internals/reduxStoreRef.ts @@ -0,0 +1,8 @@ +import { Store } from 'redux'; + +// This acts like a React ref, but it's not +const reduxStoreRef: { c: Store | null } = { + c: null, +}; + +export default reduxStoreRef; diff --git a/src/internals/typeDeclarations.d.ts b/src/internals/typeDeclarations.d.ts deleted file mode 100644 index 5f32bb8..0000000 --- a/src/internals/typeDeclarations.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Microbundle handles this -declare const __DEV__: boolean; - -declare module '@ngard/tiny-get' { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - export function get(object: any, path: string | Array, defaultValue?: any): any; -} diff --git a/src/internals/types.ts b/src/internals/types.ts index b7ee431..9c2c623 100644 --- a/src/internals/types.ts +++ b/src/internals/types.ts @@ -1,6 +1,11 @@ +declare global { + // Microbundle handles this + const __DEV__: boolean; +} + export type ChangeEntry = [string, ReduxState]; -// These renames are for readability, and to avoid having to repeatedly disable no-explicit-any when used. +// These renames are for readability, and to avoid having to repeatedly disable no-explicit-any for each usage. // eslint-disable-next-line @typescript-eslint/no-explicit-any export type DefaultReturnType = any; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/selectorFromReselect.ts b/src/selectorFromReselect.ts index eeaccef..c920a4e 100644 --- a/src/selectorFromReselect.ts +++ b/src/selectorFromReselect.ts @@ -1,21 +1,19 @@ import { RecoilValueReadOnly, selector } from 'recoil'; -import { internalStateAtom } from './internals'; -import { DefaultReturnType, ReduxState } from './internals/types'; +import { DefaultReturnType, ReduxState, reduxStateAtom } from './internals'; -let count = 0; +let selectorCount = 0; const selectorFromReselect = ( selectorFn: (reduxState: ReduxState) => ReturnType, ): RecoilValueReadOnly => { - count++; + selectorCount++; const wrappedSelector = selector({ - key: `redux-to-recoil:selector:${count}${selectorFn.name}`, + key: `redux-to-recoil:selector:${selectorCount}${selectorFn.name}`, get: ({ get }) => { - const reduxState = get(internalStateAtom); - const value = selectorFn(reduxState); - return value; + const reduxState = get(reduxStateAtom); + return selectorFn(reduxState); }, }); diff --git a/src/syncChangesFromRecoil.ts b/src/syncChangesFromRecoil.ts index f92a238..ba514ce 100644 --- a/src/syncChangesFromRecoil.ts +++ b/src/syncChangesFromRecoil.ts @@ -1,11 +1,11 @@ import { Reducer } from 'redux'; -import { SYNC_CHANGES_FROM_RECOIL, applyChangesToState } from './internals'; +import { SYNC_CHANGES_FROM_RECOIL, applyChangesToObject } from './internals'; const syncChangesFromRecoil = (rootReducer: Reducer): Reducer => { return (state, action) => { if (action.type === SYNC_CHANGES_FROM_RECOIL) { - return applyChangesToState(state, action.payload); + return applyChangesToObject(state, action.payload); } else { return rootReducer(state, action); } diff --git a/tests/helpers.tsx b/tests/helpers.tsx index 66565a8..8b42866 100644 --- a/tests/helpers.tsx +++ b/tests/helpers.tsx @@ -1,8 +1,9 @@ -import { ReduxState } from '../src/internals'; -import { AnyAction, Store, createStore } from 'redux'; import React from 'react'; -import syncChangesFromRecoil from '../src/syncChangesFromRecoil'; +import { AnyAction, Store, createStore } from 'redux'; import { Provider } from 'react-redux'; + +import { ReduxState } from '../src/internals'; +import syncChangesFromRecoil from '../src/syncChangesFromRecoil'; import SyncReduxToRecoil from '../src/SyncReduxToRecoil'; const VALUE1_DEFAULT = 100; diff --git a/tests/readState.test.tsx b/tests/readState.test.tsx index 2ffbca6..d5900a5 100644 --- a/tests/readState.test.tsx +++ b/tests/readState.test.tsx @@ -64,6 +64,15 @@ describe('read Redux state through Recoil', () => { expect(atomWithDot).toStrictEqual(atomWithoutDot); }); + it('caches atomFromRedux instances', () => { + const value1Atom: RecoilState = atomFromRedux('.value1'); + const value2Atom: RecoilState = atomFromRedux('.value2'); + const value1Atom2: RecoilState = atomFromRedux('.value1'); + + expect(value1Atom).not.toEqual(value2Atom); + expect(value1Atom).toStrictEqual(value1Atom2); + }); + it('always sees the current Redux values', () => { const value2Atom: RecoilState = atomFromRedux('value2'); const value2AtomHook = () => useRecoilValue(value2Atom);