How to solve the circular dependency problem in an nx workspace with react and and a redux store?
If for example I have a lib with all the store configuration there is a file createRootReducer - which combines my reducers:
import {
someSliceReducer,
SOME_SLICE_FEATURE_KEY,
} from '@app/feature';
export const createRootReducer = (history: History) =>
combineReducers({
router: connectRouter(history),
form: formReducer,
[SOME_SLICE_FEATURE_KEY]: someSliceReducer,
});
the application state in the store lib would be:
import { createRootReducer } from './createRootReducer';
export type ApplicationState = ReturnType<ReturnType<typeof createRootReducer>>;
The problem comes from importing the application state in the lib where the someSliceReducer lives:
import {ApplicationState} from '@app/store'
const entities= useSelector(
(state: ApplicationState) => state.dischargeFolder.loadingStatus
);
now we have a circular dependency and nx warns us about it with:
Circular dependency between "feature" and "store" detected: feature -> store -> feature eslint@nrwl/nx/enforce-module-boundaries
How could this be prevented? I need to import the reducer in the root-reducer, but i also need to import the ApplicationState into the feature. I tried to extract the root-reducer into an own lib - but this doesnt sove the problem nx complains about a circular dependendcy over 3 libs.
I also tried to have "reducerRegistry" which takes an reducer and registers it internally - unfortunatly with this approach I lose the type information of the registered reducer and the ApplicationState is not correctly infered.
If your selectors relate to only one slice then they don't need to know about the entire state. They just need to know about the type of their slice's state and know to extract that state from the entire app state.
Using the counter example from the docs, the key is that if we are adding all slices as top-level reducers (not nested) then we know the app state fulfills {counter: CounterState}
and we don't care about any other slices or what their states are.
There are a few patterns that I have used to implement this. The one that I like the best at the moment is to define the selectors for a particular slice such that they just select from their own slice state. Obviously you cannot call useSelector
with that selector, so I create a custom hook for each slice. That hook can actually live in the folder of that slice.
export const useCounterSelector = <T>(selector: (state: CounterState) => T): T =>
useSelector(({counter}: {counter: CounterState}) => selector(counter))
If you are dealing with a lot of slices you can make a generic version of that. Here I am passing in the slice itself as the argument in order to get the name (which is assumed to be its key in the main reducer) and the state type.
export const createUseSelector = <Name extends string, SliceState>(slice: Slice<SliceState, any, Name>) =>
<T>(selector: (state: SliceState) => T): T =>
useSelector((state: {[K in Name]: SliceState}) => selector(state[slice.name]))
export const useCounterSelector = createUseSelector(counterSlice);
// has inferred type <T>(selector: (state: CounterState) => T) => T