'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