Redux-toolkit: ReferenceError: can't access lexical declaration before initialization when using createAsyncThunk with createReducer

Created on 10 Aug 2020  路  9Comments  路  Source: reduxjs/redux-toolkit

Environment

"@reduxjs/toolkit": "^1.4.0",
"react": "^16.13.1",
"react-redux": "^7.2.1",
"typescript": "~3.7.2"

Sample project with issue reproduced

ghostwriternr/rtk-dependecy-example

Issue

I'm working on migrating an existing codebase to use RTK and ran into this issue. I want to use createAsyncThunk using Axios for making my API request. I have an environment state in Redux with 2 values, sandbox or prod, and corresponding to each, set the appropriate header for my axios instance.

Previously, this used to work because my reducer didn't have any dependency on the action creator. But now, after introducing createAsyncThunk, the reducer has a dependency on this thunk action creator, which in turn depends on my axios instance and further, the store itself. I believe this is causing a cyclic dependency.

Relevant code

axiosHelper.ts

import axios from "axios";

import { store } from "../store";

let defaultAxiosInstance = axios.create({
    baseURL: "https://pokeapi.co/api/v2/",
});

defaultAxiosInstance.interceptors.request.use((config) => {
    // Real application modifies the header based on environment (sandbox/prod) present in Redux state
    if (!store.getState().dummySlice.pokemon) {
        console.log("No pokemon yet");
    }
    return config;
});

export { defaultAxiosInstance };

dummyService.ts

import { AxiosResponse } from "axios";

import { defaultAxiosInstance } from "../../helper/axiosHelper";
import { PokemonResponse } from "./types";

const getPikachu = (): Promise<AxiosResponse<PokemonResponse>> => {
    return defaultAxiosInstance.get("pokemon/pikachu");
};

export const dummyService = {
    getPikachu,
};

effects.ts

import { createAsyncThunk } from "@reduxjs/toolkit";
import { dummyService } from "./dummyService";

export const fetchApiResponse = createAsyncThunk(
    "dummy/fetchApiResponse",
    async () => {
        const response = await dummyService.getPikachu();
        return { apiResponse: response.data };
    }
);

reducer.ts

import { createReducer } from "@reduxjs/toolkit";
import { fetchApiResponse } from "./effects";
import { PokemonResponse } from "./types";

const initialState: { pokemon: PokemonResponse | undefined } = {
    pokemon: undefined,
};

export const dummyReducer = createReducer(initialState, (builder) =>
    builder.addCase(fetchApiResponse.fulfilled, (draft, action) => {
        const { apiResponse } = action.payload;
        draft.pokemon = apiResponse;
    })
);

Error:

ReferenceError: can't access lexical declaration 'fetchApiResponse' before initialization
./src/store/dummy/effects.ts/<
src/store/dummy/effects.ts:1

> 1 | import { createAsyncThunk } from "@reduxjs/toolkit";
  2 | import { dummyService } from "./dummyService";
  3 | 
  4 | export const fetchApiResponse = createAsyncThunk(


Click to view full stacktrace

```typescript
ReferenceError: can't access lexical declaration 'fetchApiResponse' before initialization
./src/store/dummy/effects.ts/<
src/store/dummy/effects.ts:1

1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import { dummyService } from "./dummyService";
3 |
4 | export const fetchApiResponse = createAsyncThunk(

./src/store/dummy/reducer.ts/dummyReducer<
src/store/dummy/reducer.ts:9

6 | pokemon: undefined,
7 | };
8 |

9 | export const dummyReducer = createReducer(initialState, (builder) =>
10 | builder.addCase(fetchApiResponse.fulfilled, (draft, action) => {
11 | const { apiResponse } = action.payload;
12 | draft.pokemon = apiResponse;

executeReducerBuilderCallback
src/mapBuilders.ts:128

125 | return builder
126 | }
127 | }

128 | builderCallback(builder)
129 | return [actionsMap, actionMatchers, defaultCaseReducer]
130 | }
131 |

createReducer
src/createReducer.ts:128

125 | defaultCaseReducer?: CaseReducer
126 | ): Reducer {
127 | let [actionsMap, finalActionMatchers, finalDefaultCaseReducer] =

128 | typeof mapOrBuilderCallback === 'function'
129 | ? executeReducerBuilderCallback(mapOrBuilderCallback)
130 | : [mapOrBuilderCallback, actionMatchers, defaultCaseReducer]
131 |

./src/store/dummy/reducer.ts
src/store/dummy/reducer.ts:9

6 | pokemon: undefined,
7 | };
8 |

9 | export const dummyReducer = createReducer(initialState, (builder) =>
10 | builder.addCase(fetchApiResponse.fulfilled, (draft, action) => {
11 | const { apiResponse } = action.payload;
12 | draft.pokemon = apiResponse;

__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/store/rootReducer.ts
src/store/rootReducer.ts:1

1 | import { combineReducers } from "@reduxjs/toolkit";
2 | import { dummyReducer } from "./dummy/reducer";
3 |
4 | export const rootReducer = combineReducers({ dummySlice: dummyReducer });

__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/store/index.ts
src/store/index.ts:1

1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { rootReducer } from "./rootReducer";
3 |
4 | export const store = configureStore({ reducer: rootReducer });

__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/helper/axiosHelper.ts
src/helper/axiosHelper.ts:1

1 | import axios from "axios";
2 |
3 | import { store } from "../store";
4 |

__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/store/dummy/dummyService.ts
src/store/dummy/dummyService.ts:3

1 | import { AxiosResponse } from "axios";
2 |

3 | import { defaultAxiosInstance } from "../../helper/axiosHelper";
4 | import { PokemonResponse } from "./types";
5 |
6 | const getPikachu = (): Promise> => {

__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/store/dummy/effects.ts
src/store/dummy/effects.ts:1

1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import { dummyService } from "./dummyService";
3 |
4 | export const fetchApiResponse = createAsyncThunk(

__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/App.tsx
http://localhost:3000/static/js/main.chunk.js:110:97
__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

./src/index.tsx
http://localhost:3000/static/js/main.chunk.js:277:81
__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

fn
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:150

147 | );
148 | hotCurrentParents = [];
149 | }

150 | return __webpack_require__(request);
| ^ 151 | };
152 | var ObjectFactory = function ObjectFactory(name) {
153 | return {

1
http://localhost:3000/static/js/main.chunk.js:546:18
__webpack_require__
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:784

781 | };
782 |
783 | // Execute the module function

784 | modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
| ^ 785 |
786 | // Flag the module as loaded
787 | module.l = true;

checkDeferredModules
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:45

42 | }
43 | if(fulfilled) {
44 | deferredModules.splice(i--, 1);

45 | result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
| ^ 46 | }
47 | }
48 |

webpackJsonpCallback
/Users/nareshr/setu/rtk-dependecy-example/webpack/bootstrap:32

29 | deferredModules.push.apply(deferredModules, executeModules || []);
30 |
31 | // run deferred modules when all chunks ready

32 | return checkDeferredModules();
| ^ 33 | };
34 | function checkDeferredModules() {
35 | var result;

(anonymous function)
http://localhost:3000/static/js/main.chunk.js:1:95
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser鈥檚 developer console to further inspect this error. Click the 'X' or hit ESC to dismiss this message.
```

How can I go about resolving this? Please do let me know if I'm missing something trivial or need to provide further information.

Credits

Thanks for all the great work behind this package! Has made working with React + Redux such a breeze.

Most helpful comment

Hi there!

Fun stuff first... here is a working version of your repro: https://codesandbox.io/s/nifty-morning-5nndm?file=/src/store/index.ts

With a service like this, it's recommended to inject it into the ThunkApiConfig via extraArgument. You can do this by customizing getDefaultMiddleware

As a side note, you'd want to consider adding types for AppDispatch, as well as using a typed selector hook.

All 9 comments

Hi there!

Fun stuff first... here is a working version of your repro: https://codesandbox.io/s/nifty-morning-5nndm?file=/src/store/index.ts

With a service like this, it's recommended to inject it into the ThunkApiConfig via extraArgument. You can do this by customizing getDefaultMiddleware

As a side note, you'd want to consider adding types for AppDispatch, as well as using a typed selector hook.

Yeah, the real problem here is directly importing store into your Axios config file, and also import the Axios config into other files.

As a general rule, never import the store instance directly into other files besides the app root/index.

This does bring up the question of how you _can_ get the store instance for use in that interceptor. A couple possibilities:

  • Have the interceptor file export a function that accepts store and creates the interceptor, and import and call that in index.js. Alternately, just create the interceptor in the index.
  • Have the interceptor file export a setStore() or setupInterceptor() function, and have the index call that and inject the store after creation.

I can't believe I forgot to show that as well 馃う . I updated that CSB I linked with those points - thanks @markerikson

Thank you so much for the pointers @msutkowski and @markerikson! Importing state to setup Axios was indeed the issue.
I went ahead and removed direct imports of store everywhere except root & index in my project (as well as the repro) and my issue is resolved 馃帀

Just in case anyone stumbles onto this issue in the future, here's the gist of changes I needed:
axiosHelper.ts

import axios from "axios";
import { Store } from "@reduxjs/toolkit";

let defaultAxiosInstance = axios.create({
    baseURL: "https://pokeapi.co/api/v2/",
});

const setupInterceptors = (store: Store) => {
    defaultAxiosInstance.interceptors.request.use((config) => {
        // Real application modifies the header based on environment (sandbox/prod) present in Redux state
        if (!store.getState().dummySlice.pokemon) {
            console.log("No pokemon yet");
        }
        return config;
    });
};

export { defaultAxiosInstance, setupInterceptors };

index.tsx

...
import { store } from "./store";
import { setupInterceptors } from "./helper/axiosHelper";

setupInterceptors(store);

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById("root")
);
...

Sidenote: @msutkowski special thanks for re-creating on CSB & pointing me towards typing dispatch & selectors better. I have incorporated those in our codebase as well :grin:

Hey @markerikson @msutkowski, in the joy of finally seeing my view render again with the above fix after a full day of breaking my head, I missed noticing one of the main reasons why the solution isn't ideal. setupInterceptors is called only at the time initialization, so the interceptors are configured with the default Redux state.

But the main reason why we use state in axiosHelper.ts is so that we can dynamically set our headers based on some state value. To be more specific, the simplest use case we have is where we set a x-env header with value sandbox or prod based on the corresponding environment state set in Redux. We have several axios instances with more state-specific interceptor logic defined and exported from the same axiosHelper.ts file.

If I had to expand on the current proposed solution, I'd have to export a function to update each of these interceptors whenever the corresponding state values change, which becomes very tedious and easy to miss. Similarly, middleware isn't ideal too since I'd then have to set the interceptor based on the action type - which can again lead to errors if new action types aren't added to the list constantly.

I extended the sample project to reflect the issue again as below.

With the setupInterceptors solution, if my initial state is {isEvolved: false, pokemon: undefined}, I get "pikachu" on first render but the interceptor will never update even after I toggle isEvolved.

axiosHelper.ts

import axios from "axios";
import { store } from "../store";

let defaultAxiosInstance = axios.create({
    baseURL: "https://pokeapi.co/api/v2/pokemon",
});

defaultAxiosInstance.interceptors.request.use((config) => {
    // Real application modifies the header based on environment (sandbox/prod) present in Redux state
    const { isEvolved } = store.getState().dummySlice;
    if (isEvolved) {
        config.url = "/raichu";
    } else {
        config.url = "/pikachu";
    }
    return config;
});

export { defaultAxiosInstance };

@ghostwriternr I'm not sure I exactly understand the issue at this point.

The original problem you described was a cyclical dependency issue. Switching to exporting and calling setupInterceptors fixes that.

I'm not familiar with how Axios interceptors work, but my assumption looking at your example defaultAxiosInstance.interceptors.request.use() sets up something akin to an Express middleware, where the function runs for every API call made.

In that case, the store.getState() call _should_ be getting run for each API call as well, and thus the interceptor is able to make decisions based on the current state at the time of the API call.

Can you clarify what the intended behavior is here, and what you're seeing happen instead? What do you mean by "the interceptor will never update" ?

To tack on to 鈽濓笍 , I just tested this and it behaves as you're describing your desired outcome, and works as @markerikson explains. The interceptors are run on every request, so the store.getState() will be at it's the latest snapshot on execution. To demo, search for pikachu, click 'Set evolved', search again.

https://codesandbox.io/s/nifty-morning-5nndm?file=/src/helper/axiosHelper.ts

Note: it is broken due to config.url not being valid, but shows it reacting to store changes

I embarrass myself, you guys are absolutely right. Switching to exporting and calling setupInterceptors does indeed fix the cyclic dependency and also works as I expect it to in my re-opened comment. I let an unrelated bug slip-in while testing the solution and correlated it back to this.

My fundamental understanding of the initialization order + how imports & functions work in Javascript are incorrect and I'll use this exercise as an excuse to go back and read more about these now. Thank you so much being patient enough to reproduce the behaviour and explain it in depth ^ . ^

You're welcome, and no worries :) Glad this is working for you!

Was this page helpful?
0 / 5 - 0 ratings