'How to fix circular dependencies of slices with the RootState?
I recently started using redux-toolkit and started writing my reducers using the createSlice
following their docs.
One reducer, let's call it reducerA
, imports customAsyncFunction
to handle its callback, this function is created through createAsyncThunk
which in turn reads the RootState
when it calls thunkApi.getState()
, the problem now is that when RootReducer
is imported, reducerA
will be imported generating a circular reference.
Basically: RootReducer -> reducerA -> actions -> RootReducer -> ...
Below I attempt to simplify the problem.
// actions.ts file
import { RootState } from "./RootReducer";
export const customAsyncAction = createAsyncAction("myaction", async (_, thunkApi) =>
const state = thinkApi.getState() as RootState;
...
);
// reducerA.ts file
import { customAsyncAction } from "./actions";
const slice = createSlice({
...
extraReducers: {
[customAsyncAction.fulfilled.toString()]: ... // handles fulfilled action
}
});
export default slice.reducer;
// RootReducer.ts file
import reducerA from "./reducerA"
import reducerB from "./reducerB"
const reducers = combineReducers({
reducerA,
reducerB
});
export type RootState = ReturnType<typeof reducers>; // complains about circular reference
In this section of the documentation it's mentioned the likelihood of this happening and there are vague suggestions of splitting the code in files. However from all my attempts I can't seem to find a way to fix this problem.
Solution 1:[1]
After reading the following docs section on how to use createAsyncThunk
with TypeScript, I've changed the implementation of extraReducers
to use the builder
pattern instead of passing a key value object and the error vanished.
// before
const slice = createSlice({
...
extraReducers: {
[customAsyncAction.fulfilled.toString()]: ... // handles fulfilled action
}
});
// after
const slice = createSlice({
...
extraReducers: builder => {
builder.addCase(customAsyncAction.fulfilled, (state, action) => ...)
}
});
I must admit I can't pinpoint exactly why under the first condition it doesn't works, while in the second one it does.
Solution 2:[2]
Babel thinks the import is a module, and don't know it's a type and it's totally fine to import it. In order to tell it's a type import Try importing it as a type:
import type { RootState } from "./RootReducer";
Note the type
keyword after import
. This way babel/eslint know you import a type, not a module and will exclude it from dependency map, thus resolving the issue.
Solution 3:[3]
Type-only circular references are fine. The TS compiler will resolve those at compile time. In particular, it's normal to have a slice file export its reducer, import the reducer into the store setup, define the RootState
type based on that slice, and then re-import the RootState
type back into a slice file.
Circular imports are only a potential issue when runtime behavior is involved, such as two slices depending on each other's actions.
Unfortunately, the ESLint rule for catching circular dependencies can't tell that what's being imported is just a type, as far as I know.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Rigotti |
Solution 2 | Nivethan |
Solution 3 | markerikson |