Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot import redux-toolkit from a Node.js ESM module #1960

Closed
cdauth opened this issue Jan 25, 2022 · 33 comments
Closed

Cannot import redux-toolkit from a Node.js ESM module #1960

cdauth opened this issue Jan 25, 2022 · 33 comments
Milestone

Comments

@cdauth
Copy link

cdauth commented Jan 25, 2022

I have a bit of an unusual setup where I use redux-toolkit also in the backend of a Node.js application. I am currently in the process of migrating my backend to ESM modules, since some dependencies (in particular node-fetch) are starting to ship only ESM modules.

Error description

When I try to import redux-toolkit in an mjs module using import { createSlice } from '@reduxjs/toolkit';, I am receiving the following error:

SyntaxError: Named export 'createSlice' not found. The requested module '@reduxjs/toolkit' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '@reduxjs/toolkit';
const { createSlice } = pkg;

The workaround suggested in the error message does work for Node.js mjs files. The problem is that the code where I use redux-toolkit is shared by the backend (running on Node.js) and the frontend (compiled using webpack). With the workaround in place, webpack can now not compile the file anymore and gives the following error:

export 'default' (imported as 'toolkit') was not found in '@reduxjs/toolkit' (possible exports: MiddlewareArray, __DO_NOT_USE__ActionTypes, applyMiddleware, bindActionCreators, combineReducers, compose, configureStore, createAction, createAsyncThunk, createDraftSafeSelector, createEntityAdapter, createImmutableStateInvariantMiddleware, createNextState, createReducer, createSelector, createSerializableStateInvariantMiddleware, createSlice, createStore, current, findNonSerializableValue, freeze, getDefaultMiddleware, getType, isAllOf, isAnyOf, isAsyncThunkAction, isDraft, isFulfilled, isImmutableDefault, isPending, isPlain, isPlainObject, isRejected, isRejectedWithValue, miniSerializeError, nanoid, original, unwrapResult)

Reason for the error

redux-toolkit is bundled in several different formats, among them cjs and esm. The bundles are referenced in package.json in the following way (index.js being a wrapper that includes the cjs bundle):

  "main": "dist/index.js",
  "module": "dist/redux-toolkit.esm.js",

While the module property is probably supported by webpack and other bundlers, it does not seem to be supported by Node.js. Instead, Node.js uses the exports property to support different main files for different environments (see here. Since that is not defined in this case, Node.js requires the file from the main property, which is a CommonJS bundle.

Even forcing Node.js to use the ESM bundle by doing import { createSlice } from '@reduxjs/toolkit/dist/redux-toolkit.esm.js'; does not solve the problem. The problem is that Node.js interprets files as CommonJS unless they have a .jsm file extension or "type": "module" is defined in package.json (which then applies to all files in the package) (see here).

Node.js does support importing CommonJS packages in most cases, but in the case of redux-toolkit for some reason it doesn’t work. I am not sure why, but none of my other dependencies had this problem.

Possible solution

Setting "type": "module" is probably not an option, since that will break the CommonJS files.

The only solution that I can think of is to ship the ESM bundle as an .mjs file, either by renaming the current one or by creating a copy. The file can then be referenced in package.json like this:

  "exports": {
    "import": "./dist/redux-toolkit.esm.mjs",
    "require": "./dist/index.js"
  },

This solution does not solve the problem completely, as redux-toolkit uses immer, which has a similar problem. I have reported that as immerjs/immer#901.

Workaround

Use the import like this:

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = toolkitRaw.default ?? toolkitRaw;

or in Typescript:

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = ((toolkitRaw as any).default ?? toolkitRaw) as typeof toolkitRaw;
@markerikson
Copy link
Collaborator

Yes, we do not currently ship our package in a fully Node-ESM compatible way. (Frankly this entire situation has been incredibly confusing for me every time I try to do research on it.)

I've been advised that making any changes to our setup along those lines, such as adding an exports field, would be considered a breaking change that requires a major version. As such, it's not something we can really even consider until whenever we get around to working on an RTK 2.0 version, and currently there's no specific timeline for when we'll try to do that.

See #958 for some related notes.

@cdauth
Copy link
Author

cdauth commented Jan 25, 2022

I am also really confused about ES modules in Node.js, and every time I attempt the migration I give up at some point out of frustration.

I was not aware that adding an exports field is a breaking change. But I just tried it and can confirm that adding one makes it impossible to import any sub-paths of the package. That sucks.

As an intermediate solution I can only imagine generating an .mjs file without referencing it in package.json. That would at least allow me to import '@reduxjs/toolkit/dist/redux-toolkit.esm.mjs';. It is not a good solution, but at least it would allow to use redux-toolkit in a Node.js ESM application at all.

@cdauth
Copy link
Author

cdauth commented Jan 26, 2022

I found a workaround that works in both backend and frontend:

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = toolkitRaw.default ?? toolkitRaw;

or in Typescript:

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = ((toolkitRaw as any).default ?? toolkitRaw) as typeof toolkitRaw;

@toychicken
Copy link

Just FYI - all the examples on the Redux Toolkit site uses ESM imports - might be why some are confused here! e.g. https://redux-toolkit.js.org/api/createReducer

@markerikson
Copy link
Collaborator

@toychicken Yes, because ESM syntax has been pretty standard for writing client-side code for several years now. The Node situation is unfortunate, but not a reason to skip showing ESM imports in examples.

@carere
Copy link

carere commented May 7, 2022

Hello, just wonder if the PR associated with this issue will be included in the next minor / patch realease of redux toolkit ?
It'll help a lot in order to work with Redux Toolkit in Node (especially when testing with node, which is the main flaw actually) 😄

@karmaniverous
Copy link

If you're testing ES6 code referencing Redux in a transpiled build environment and have installed the new JSX Transform, you can save yourself a lot of trouble by putting your tests into JSX files. Explanation here.

For the record, I'm aware of how dumb that sounds. 🤷‍♂️

@markerikson
Copy link
Collaborator

As a follow-up: after some additional discussion, @karmaniverous and I concluded that what's actually going on is that Mocha in that template repo was configured to auto-register Babel, and a full set of Babel transforms is being run on any .jsx file. This includes @babel/preset-env, which transpiles modules from ESM to CJS by default. So, renaming files to .jsx unfortunately isn't a magic solution here :)

@muhammedalibilgin
Copy link

thank you, i got the same error, and this worked for me, i solved my problem.👍

@romaricpascal
Copy link

romaricpascal commented Aug 12, 2022

If that can be helpful, I've managed to get Redux Toolkit imported properly when running in Mocha through a little patching with patch-package, to:

  • update the package.json file to declare exports as described earlier in the thread
  • fix the use of the thunkMiddleware which gets accessed using thunkMiddleware.default when running inside Node.

Note that the project it runs in has "type": "module" in its package.json (as well as the "postinstall": "patch-package" script required by patch-package, of course)

In case it helps anyone, this is the patches/@reduxjs+toolkit+1.8.4.patch patch file that got generated by patch-package:

diff --git a/node_modules/@reduxjs/toolkit/dist/redux-toolkit.esm.js b/node_modules/@reduxjs/toolkit/dist/redux-toolkit.esm.js
index 95beed1..b0d0530 100644
--- a/node_modules/@reduxjs/toolkit/dist/redux-toolkit.esm.js
+++ b/node_modules/@reduxjs/toolkit/dist/redux-toolkit.esm.js
@@ -420,10 +420,10 @@ function getDefaultMiddleware(options) {
     var middlewareArray = new MiddlewareArray();
     if (thunk) {
         if (isBoolean(thunk)) {
-            middlewareArray.push(thunkMiddleware);
+            middlewareArray.push((thunkMiddleware.default || thunkMiddleware));
         }
         else {
-            middlewareArray.push(thunkMiddleware.withExtraArgument(thunk.extraArgument));
+            middlewareArray.push((thunkMiddleware.default || thunkMiddleware).withExtraArgument(thunk.extraArgument));
         }
     }
     if (process.env.NODE_ENV !== "production") {
diff --git a/node_modules/@reduxjs/toolkit/package.json b/node_modules/@reduxjs/toolkit/package.json
index 460a8c0..3680368 100644
--- a/node_modules/@reduxjs/toolkit/package.json
+++ b/node_modules/@reduxjs/toolkit/package.json
@@ -118,5 +118,10 @@
   "bugs": {
     "url": "https://github.com/reduxjs/redux-toolkit/issues"
   },
-  "homepage": "https://redux-toolkit.js.org"
+  "homepage": "https://redux-toolkit.js.org",
+  "type": "module",
+  "exports": {
+    "import": "./dist/redux-toolkit.esm.js",
+    "require": "./dist/index.js"
+  }
 }

If you're looking to patch the library further (for ex. to remove the enableES5() call), seems patch-package can only handle one patch per library, so you'll need to make sure to generate your own patch after editing further. To make sure the changes in package.json get in the patch, there's a little twist and you'll need to add the --exclude flag for generating the patch : npx patch-package --exclude.

@markerikson
Copy link
Collaborator

fix the use of the thunkMiddleware which is a CommonJS package by the looks of it

that sounds odd - we definitely ship an ESM file in the redux-thunk package, listed under the module key, same as our other libs:

https://unpkg.com/browse/redux-thunk@2.4.1/package.json

@chentsulin
Copy link

chentsulin commented Aug 12, 2022

Our team uses gen-esm-wrapper to create another .mjs file (koa uses this approach too) and then define a exports field in the package.json file. This patch makes us safely use @reduxjs/redux-toolkit in our ESM jest tests:

diff --git a/dist/redux-toolkit.esm.mjs b/dist/redux-toolkit.esm.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..a0660c23c131aee148178a8b9a5e28252b6d00b7
--- /dev/null
+++ b/dist/redux-toolkit.esm.mjs
@@ -0,0 +1,47 @@
+import mod from "./index.js";
+
+export default mod;
+export const MiddlewareArray = mod.MiddlewareArray;
+export const TaskAbortError = mod.TaskAbortError;
+export const __DO_NOT_USE__ActionTypes = mod.__DO_NOT_USE__ActionTypes;
+export const addListener = mod.addListener;
+export const applyMiddleware = mod.applyMiddleware;
+export const bindActionCreators = mod.bindActionCreators;
+export const clearAllListeners = mod.clearAllListeners;
+export const combineReducers = mod.combineReducers;
+export const compose = mod.compose;
+export const configureStore = mod.configureStore;
+export const createAction = mod.createAction;
+export const createAsyncThunk = mod.createAsyncThunk;
+export const createDraftSafeSelector = mod.createDraftSafeSelector;
+export const createEntityAdapter = mod.createEntityAdapter;
+export const createImmutableStateInvariantMiddleware = mod.createImmutableStateInvariantMiddleware;
+export const createListenerMiddleware = mod.createListenerMiddleware;
+export const createNextState = mod.createNextState;
+export const createReducer = mod.createReducer;
+export const createSelector = mod.createSelector;
+export const createSerializableStateInvariantMiddleware = mod.createSerializableStateInvariantMiddleware;
+export const createSlice = mod.createSlice;
+export const createStore = mod.createStore;
+export const current = mod.current;
+export const findNonSerializableValue = mod.findNonSerializableValue;
+export const freeze = mod.freeze;
+export const getDefaultMiddleware = mod.getDefaultMiddleware;
+export const getType = mod.getType;
+export const isAllOf = mod.isAllOf;
+export const isAnyOf = mod.isAnyOf;
+export const isAsyncThunkAction = mod.isAsyncThunkAction;
+export const isDraft = mod.isDraft;
+export const isFulfilled = mod.isFulfilled;
+export const isImmutableDefault = mod.isImmutableDefault;
+export const isPending = mod.isPending;
+export const isPlain = mod.isPlain;
+export const isPlainObject = mod.isPlainObject;
+export const isRejected = mod.isRejected;
+export const isRejectedWithValue = mod.isRejectedWithValue;
+export const legacy_createStore = mod.legacy_createStore;
+export const miniSerializeError = mod.miniSerializeError;
+export const nanoid = mod.nanoid;
+export const original = mod.original;
+export const removeListener = mod.removeListener;
+export const unwrapResult = mod.unwrapResult;
diff --git a/package.json b/package.json
index 25de569f85e80553a03d3fa8abc80707ff7f8501..a836b4a1ef4cac9dafaacf369d94fecd1d4fa202 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,17 @@
     "access": "public"
   },
   "main": "dist/index.js",
   "module": "dist/redux-toolkit.esm.js",
   "unpkg": "dist/redux-toolkit.umd.min.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "module": "./dist/redux-toolkit.esm.js",
+      "import": "./dist/redux-toolkit.esm.mjs",
+      "require": "./dist/index.js"
+    }
+  },
   "devDependencies": {
     "@microsoft/api-extractor": "^7.13.2",
     "@size-limit/preset-small-lib": "^4.11.0",

@chentsulin
Copy link

Or if we want to let @reduxjs/redux-toolkit cjs available for named import in ESM files, we need to make it cjs-module-lexer analyzable.

@romaricpascal
Copy link

romaricpascal commented Aug 22, 2022

@markerikson From what I understood of the start of the thread, Node seems to rely on the "exports" field to pick the files to import. I'm not sure if the "module" field is something it understands or if it's just used by bundlers.

fix the use of the thunkMiddleware which is a CommonJS package by the looks of it

that sounds odd - we definitely ship an ESM file in the redux-thunk package, listed under the module key, same as our other libs:

https://unpkg.com/browse/redux-thunk@2.4.1/package.json

@markerikson
Copy link
Collaborator

Right, I'm just saying that redux-thunk is not strictly a "CommonJS package". It does include multiple module formats in the published build artifact.

@romaricpascal
Copy link

Oh right, apologies for the hasty shortcut there, I'll edit my message to not mislead 😊

Right, I'm just saying that redux-thunk is not strictly a "CommonJS package". It does include multiple module formats in the published build artifact.

@akselikap
Copy link

This gets even more confusing with RTK Query. I have a similar situation where I use RTK on both backend and frontend. When using ESM, TypeScript can't locate declaration files for RTK Query so in the end I ended up doing:

import * as rtkQuery from '@reduxjs/toolkit/dist/query/index.js';
const { buildCreateApi, coreModule, fetchBaseQuery } = ((rtkQuery as any).default ?? rtkQuery) as typeof rtkQuery;
import * as rtkQueryReact from '@reduxjs/toolkit/dist/query/react/index.js';
const { reactHooksModule } = ((rtkQueryReact as any).default ?? rtkQueryReact) as typeof rtkQueryReact;
const createApi = buildCreateApi(
    coreModule(),
    reactHooksModule(),
);

I had to build createApi myself because if I didn't all RTK Query types would fail to infer. Also I still get webpack warnings so I had to ignore those explicitly in my webpack config as well. Hopefully RTK 2.0 comes and saves me from this madness 😄

@markerikson
Copy link
Collaborator

Dropping a key link in here about how to improve package publishing:

https://github.com/frehner/modern-guide-to-packaging-js-library

@benmccann
Copy link

Here's another resource for improving packaging: https://kit.svelte.dev/faq#packages

And a linter that you can use: https://publint.bjornlu.com/@reduxjs/toolkit@1.8.6

@t-fritsch
Copy link

t-fritsch commented Oct 27, 2022

This gets even more confusing with RTK Query. I have a similar situation where I use RTK on both backend and frontend. When using ESM, TypeScript can't locate declaration files for RTK Query so in the end I ended up doing:

Thanks a lot @akselikap, your example saved me ! 😅

Since I'm not using TypeScript, the code I ended up with is a little bit simpler :

import * as rtkQuery from '@reduxjs/toolkit/dist/query/react/index.js';
const { createApi, fetchBaseQuery } = rtkQuery.default ?? rtkQuery;

Note that I import from '.../query/react/...' package and not '.../query/...' as in your example, because I need the auto-generated hooks for my React components.

chesterlaykin pushed a commit to chesterlaykin/fork-redux-toolkit that referenced this issue Jan 4, 2023
@markerikson
Copy link
Collaborator

Hiya, folks.

For the record, I am finally trying to get started working on proper ESM support for RTK.

Aaaaand I hate everything about this :)

@SebastianGaud
Copy link

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = toolkitRaw.default ?? toolkitRaw;

In my case even this approach doesn't work, there are more workarounds?

@markerikson
Copy link
Collaborator

@SebastianGaud : like I said, I'm working on this right now :)

@markerikson markerikson added this to the 2.0 milestone Jan 28, 2023
@markerikson
Copy link
Collaborator

This should now be available in https://github.com/reduxjs/redux-toolkit/releases/tag/v2.0.0-alpha.1 .

Please try that out and let us know if it works!

@markerikson
Copy link
Collaborator

FYI folks, I've published https://github.com/reduxjs/redux-toolkit/releases/tag/v2.0.0-alpha.4 , which does further improvements to the ESM/CJS package formatting.

I've also built up a whole suite of example apps using different build tools (CRA4/5, Next, Vite) and other test projects (Node in ESM and CJS mode, and the arethetypeswrong tool) to verify that the package works as expected in a variety of different environments.

I feel fairly good about the ESM/CJS packaging at this point. Please try out that alpha and give us feedback!

@benmccann
Copy link

It's passing publint! https://publint.dev/@reduxjs/toolkit@2.0.0-alpha.4

@markerikson
Copy link
Collaborator

I'm going to go ahead and call it done at this point, as of alpha.4.

If anyone does run into further problems, please file a new issue!

ColaFanta added a commit to ColaFanta/svelte-redux-adapter that referenced this issue Aug 28, 2023
[1]upgraded to svelte 4
[2]updated other deps
[3]compatible with redux/toolkit cjs([issue](reduxjs/redux-toolkit#1960))
@roggc
Copy link

roggc commented Sep 17, 2023

I found a workaround that works in both backend and frontend:

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = toolkitRaw.default ?? toolkitRaw;

or in Typescript:

import * as toolkitRaw from '@reduxjs/toolkit';
const { createSlice } = ((toolkitRaw as any).default ?? toolkitRaw) as typeof toolkitRaw;

what worked for me was:

import * as reduxToolkit from '@reduxjs/toolkit'; const {createSlice}=reduxToolkit?.default??reduxToolkit;

@markerikson
Copy link
Collaborator

@roggc : did you try out our RTK 2.0 betas? They should specifically address this issue.

@roggc
Copy link

roggc commented Sep 17, 2023

Hey @markerikson, thanks for letting me know. I am the author of react-context-slices, a library to manage global shared state in React that seamlessly integrates both Redux and React Context with zero-boilerplate. Recently I developed a solution (a setup) of RSC+SSR (React Server Components + Server Side Rendering) from scratch after reading this guide from Dan Abramov. Dan ended this guide by proposing challenges to people, being the last of them to incorporate RCC (React Client Components) to the solution. In this implementation of my own I used react-context-slices too, and that's where the problem appeared. As I said I used the mentioned workaround. I suppose I am not sure of using a beta now yet. When it becomes released I will update the library. Thanks again!

@azzazkhan
Copy link

azzazkhan commented Sep 30, 2023

@markerikson Is the issue resolved in the latest release? I'm trying to use the Redux Toolkit in an Inertia.js SSR app but getting errors when running the SSR script.

import { createSlice, configureStore } from "@reduxjs/toolkit";
                      ^^^^^^^^^^^^^^
SyntaxError: Named export 'configureStore' not found. The requested module '@reduxjs/toolkit' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '@reduxjs/toolkit';
const { createSlice, configureStore } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:124:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:190:5)

Node.js v18.17.1

Following is my store.ts script.

import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
    reducer: {
        // slices added here...
    },
    devTools: import.meta.env.DEV,
});

@phryneas
Copy link
Member

@azzazkhan did you see Marks last message and try the 2.0 betas?

@bever1337
Copy link
Contributor

bever1337 commented Oct 7, 2023

I can confirm that upgrading to the 2.0.0-2 beta resolves this issue for sveltekit builds. Great fix, thank y'all. No configuration options are necessary

nandito added a commit to whereby/jslib-media that referenced this issue Mar 11, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
namespace imports for uuid and `default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
nandito added a commit to whereby/jslib-media that referenced this issue Mar 11, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
namespace imports for uuid and `default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
nandito added a commit to whereby/jslib-media that referenced this issue Mar 11, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
namespace imports for uuid and `default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
nandito added a commit to whereby/jslib-media that referenced this issue Mar 12, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
namespace imports for uuid and `default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
nandito added a commit to whereby/jslib-media that referenced this issue Mar 12, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
namespace imports for uuid and `default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
nandito added a commit to whereby/jslib-media that referenced this issue Mar 12, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
`default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
nandito added a commit to whereby/jslib-media that referenced this issue Mar 13, 2024
There's a confusion in ESM and CJS, let's try to fix them by using
`default` wrapper removal for `adapter`
(as suggested here: reduxjs/redux-toolkit#1960)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.