repack icon indicating copy to clipboard operation
repack copied to clipboard

Repack app fails with "could not find react-redux context value" for the latest versions of react-redux and @reduxjs/toolkit

Open RafaelA977 opened this issue 1 year ago • 1 comments

Description

Repack React Native app fails with error "Error: could not find react-redux context value; please ensure the component is wrapped in a Provider" with the latest versions of react-redux and @reduxjs/toolkit. In the same time the clean React-Native project without Repack works fine.

Steps to reproduce:

  1. Install React-Native version of 0.73.4
  2. Add react-redux (version is 9.1.0), @reduxjs/toolkit (2.1.0).
  3. Install @callstack/repack using command npx @callstack/repack-init
  4. Run application.

Package json: { "name": "ReduxTest", "version": "0.0.1", "private": true, "scripts": { "android": "react-native run-android --no-packager --port 8087", "ios": "react-native run-ios", "lint": "eslint .", "start": "react-native webpack-start --host 127.0.0.1 --port 8087", "test": "jest" }, "dependencies": { "@react-navigation/native": "^6.1.10", "@reduxjs/toolkit": "2.1.0", "react": "18.2.0", "react-native": "0.73.4", "react-redux": "9.1.0" }, "devDependencies": { "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", "@callstack/repack": "^3.7.0", "@react-native/babel-preset": "0.73.21", "@react-native/eslint-config": "0.73.2", "@react-native/metro-config": "0.73.5", "@react-native/typescript-config": "0.73.1", "@types/react": "^18.2.6", "@types/react-test-renderer": "^18.0.0", "babel-jest": "^29.6.3", "babel-loader": "^9.1.3", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", "react-test-renderer": "18.2.0", "terser-webpack-plugin": "^5.3.10", "typescript": "5.0.4", "webpack": "^5.90.1" }, "engines": { "node": ">=18" } }

image

Reproducible Demo

I've created an example to reproduce and see the all configuration of the project - https://github.com/RafaelA977/RepackReduxExample

Please help us fix this issue.

RafaelA977 avatar Feb 12 '24 08:02 RafaelA977

Hey @RafaelA977

I managed to get it to work by doing the following in the webpack.config:

  1. In resolve field, add the following entires:
resolve: {
   ...
   alias: {
        reselect: path.resolve(
          './node_modules/reselect/dist/reselect.legacy-esm.js',
        ),
        redux: path.resolve('./node_modules/redux/dist/redux.legacy-esm.js'),
        'react-redux': path.resolve(
          './node_modules/react-redux/dist/react-redux.legacy-esm.js',
        ),
        immer: path.resolve('./node_modules/immer/dist/immer.legacy-esm.js'),
        '@reduxjs/toolkit': path.resolve(
          './node_modules/@reduxjs/toolkit/dist/redux-toolkit.legacy-esm.js',
        ),
        'redux-thunk': path.resolve(
          './node_modules/redux-thunk/dist/redux-thunk.legacy-esm.js',
        ),
      },
}
  1. In module.rules add the following to the rule for transpiling the node_modules:
rules: [
  {
    test: /\.[jt]sx?$/,
    include: [
      /node_modules(.*[/\\])+react\//,
      /node_modules(.*[/\\])+react-native/,
      /node_modules(.*[/\\])+@react-native/,
      /node_modules(.*[/\\])+@react-navigation/,
      /node_modules(.*[/\\])+@react-native-community/,
      /node_modules(.*[/\\])+expo/,
      /node_modules(.*[/\\])+pretty-format/,
      /node_modules(.*[/\\])+metro/,
      /node_modules(.*[/\\])+abort-controller/,
      /node_modules(.*[/\\])+@callstack\/repack/,
/* -> */ /node_modules(.*[/\\])+immer/,
/* -> */ /node_modules(.*[/\\])+redux/,
/* -> */ /node_modules(.*[/\\])+react-redux/,
/* -> */ /node_modules(.*[/\\])+@reduxjs(.*[/\\])+toolkit/,
/* -> */ /node_modules(.*[/\\])+redux-thunk/,
/* -> */ /node_modules(.*[/\\])+reselect/,
    ],
    use: 'babel-loader',
  },
],

This should work for now, I'll still investigate this in more detail as there should be less differences there, here's how the resolution table look for metro and webpack without adding the aliases:

Library Metro Resolved Path (metro-resolver) Webpack Resolved Path (enhanced-resolve)
react-redux node_modules/react-redux/dist/react-redux.legacy-esm.js node_modules/react-redux/dist/cjs/index.js
@reduxjs/toolkit node_modules/@reduxjs/toolkit/dist/cjs/index.js node_modules/@reduxjs/toolkit/dist/cjs/index.js
immer node_modules/immer/dist/immer.legacy-esm.js node_modules/immer/dist/cjs/index.js
redux node_modules/redux/dist/cjs/redux.cjs node_modules/redux/dist/cjs/redux.cjs
reselect node_modules/reselect/dist/cjs/reselect.cjs node_modules/reselect/dist/cjs/reselect.cjs
redux-thunk node_modules/redux-thunk/dist/cjs/redux-thunk.cjs node_modules/redux-thunk/dist/cjs/redux-thunk.cjs

Changing the entrypoint paths for react-redux & immer should be enough in this case but for some reason isn't.

jbroma avatar Mar 01 '24 16:03 jbroma

Hi @jbroma

Thank you for your investigation this issue.

Actually, I tried to add the aliases and rules before creating this issue, but I added the wrong alias paths to resolve redux dependencies.

Now it works fine with the correct alias paths.

Could you please tell me how to find the resolved path in the metro? I have the same issue for some other libraries (for example, native-base) and I want to find the correct resolved paths to add them in webpack config.

RafaelA977 avatar Mar 12 '24 13:03 RafaelA977

hey @RafaelA977,

Glad you got this to work! 🙌

I actually dug dipper into this issue and picked some actions points to implement in next Re.Pack release.

Right now in Re.Pack, we have ESM Package Exports enabled by default which causes most of these issues. By adding the following to resolve options, we can achieve the same resolutions as metro (without package exports enabled):

resolve: {
  ...Repack.getResolveOptions(platform),
  conditionNames: [],
  exportsFields: [],
}

To fix this issue more systematically, we will disable that and make it optional (via a param to getResolveOptions) to align more closely with metro. In metro there is an experimental support for Package Exports, but that also has a nice fallback mechanism to resolve using mainFields in case there is no match - this is what's missing in enhanced-resolve, which is responsible for resolutions here. This will be documented to save everybody's precious time.

Another issues is that our default template does not pick up .cjs files to transform, and that was the case in here as well. After applying the resolve adjustments above, we need to also modify rules for loading node_modules:

rules: [
  {
    test: /\.[cm]?[jt]sx?$/, // <--- add optional 'c' or 'm' to match .cjs and .mjs
    include: [
      /node_modules(.*[/\\])+react\//,
      /node_modules(.*[/\\])+react-native/,
      /node_modules(.*[/\\])+@react-native/,
      /node_modules(.*[/\\])+@react-navigation/,
      /node_modules(.*[/\\])+@react-native-community/,
      /node_modules(.*[/\\])+expo/,
      /node_modules(.*[/\\])+pretty-format/,
      /node_modules(.*[/\\])+metro/,
      /node_modules(.*[/\\])+abort-controller/,
      /node_modules(.*[/\\])+@callstack\/repack/,
      /node_modules(.*[/\\])+immer/,
      /node_modules(.*[/\\])+redux/,
      /node_modules(.*[/\\])+react-redux/,
      /node_modules(.*[/\\])+@reduxjs(.*[/\\])+toolkit/,
    ],
    use: "babel-loader",
  },
]

As to why we need to transpile .cjs as well, it mostly boils down to specific requirements set by Hermes engine, in this case it's mostly classes support, but also arrow-functions & block-scoping (these 2 might cause very hard to debug problems but in 99.9% of cases they will work the same and cause no issues at all)

You can try applying these changes and most likely your problems with other libraries will resolve themselves this way (after adding them to include modules above.

Finally, as to how to find resolved paths in metro, I've used this custom resolveRequest in metro.config.js:

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const path = require('path');
/**
 * Metro configuration
 * https://facebook.github.io/metro/docs/configuration
 *
 * @type {import('metro-config').MetroConfig}
 */
const config = {
  resolver: {
    resolveRequest: (context, moduleName, platform) => {
      if (
        !moduleName.includes('..') &&
        (moduleName.includes('@reduxjs/toolkit') ||
          moduleName.includes('react-redux') ||
          moduleName.includes('redux-thunk') ||
          moduleName.includes('reselect') ||
          moduleName.includes('immer') ||
          moduleName.includes('redux'))
      ) {
        const res = context.resolveRequest(context, moduleName, platform);
        if (moduleName) {
          console.log(
            '\n' + moduleName,
            path.relative(process.cwd(), res.filePath),
          );
        }
      }
      return context.resolveRequest(context, moduleName, platform);
    },
  },
};

module.exports = mergeConfig(getDefaultConfig(__dirname), config);

The output isn't the prettiest but it will get you what you want.

I think it might be useful to include a plugin in Re.Pack to debug resolution differences and showcase potential problems, so this kind of issues can be put to rest once and for all.

I'll close this issue since your original problem was resolved.

jbroma avatar Mar 12 '24 13:03 jbroma