-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
"{hook} may be used only in the context of a <Router> component" when running with vitest, node >=22.12 and there's a peer-dep on react-router #12785
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
Comments
I am facing the same issues after replacing |
This issue is present for me when using tsx to run a custom server in dev mode. When building and running in production, the issue is not present. I've experimented with node 22.12 and 22.13, as well as vite 5 and vite 6. The issue persists across all of them. Using react-router 7.1.3. The external package that I am using is @rvf/react-router |
Removing module-sync in node_modules/react-router/package.json does resolve the issue for me. |
True! I forgot to mention that in the description. That's indeed what fixed it for me in the original issue #12475 (comment) |
I am also facing the same issues with React Router v7 and Vite when using the new Clerk React Router SDK. Even when I use Node version 22.11 or lower, as well as Vite 5 and 6, I get this error message: I'm using these dependencies:
|
@justinwaite Do you mean i need to remove the module-sync line in node_modules/react-router/package.json ?
And then delete the cache? rm -rf node_modules/.vite |
@MichaelvdVeer based on your description, it seems to me you are facing a different scenario. Perhaps you are in fact using the hook outside of a router. |
Hi @acelaya, Thanks for your reply! I am using the ClerkProvider in main.jsx and then using createBrowserRouter in a separate file (AppRoutes.jsx). I’ve created a minimal reproduction of the issue, which you can find here: https://github.com/MichaelvdVeer/clerk-react-router-example. Could you take a look at this and let me know if I’m missing something? Thanks in advance! If needed, I will create a separate issue. |
Yes, please. Let's not derail the topic here. |
This comment has been minimized.
This comment has been minimized.
@acelaya I think I've found the root cause - since I was able to use your repo to reproduce the issue and "fix" it by applying a workaround. Looking at the bundles in
Which are referred to from the package.json exports:
These are actual bundles, meaning that the The ESM bundles ( You can verify this by editing Shell output after this modification:
|
Thanks @sergei-maertens, but yeah, the root cause was already known, and discussed in #12475 I reported this to see why with certain versions of node, and when using vitest, the wrong bundle was being resolved for packages that peer-depend on react-router. I don't know if the fix should be done in react-router or some other package. |
Ah damn, I've been reading through some many issues and still dismissed that one... IMO the bundling/packaging problem is something to fix in react-router because anything not able to use the modules is affected by this and it looks like there's no workaround for it (except for manually providing the I'm going to dive a bit deeper in the module resolution and see if I can either figure out why it's happening or if there's some Vite config that would make it possible to point it to the right imports. Thanks for the extra insights! |
Hrm, I read through the entire issue and I'm not sure if the conclusion is the same - that issue seems to be more about the My tests pass if I update my import from If the CJS bundles ( |
Okay, I didn't manage to fully understand why vitest seems to prefer the CommonJS build - maybe it's because there's no diff --git a/vite.config.mts b/vite.config.mts
index 472f0ed7..7921afa9 100644
--- a/vite.config.mts
+++ b/vite.config.mts
@@ -4,6 +4,7 @@ import {codecovVitePlugin} from '@codecov/vite-plugin';
import replace from '@rollup/plugin-replace';
import {sentryVitePlugin} from '@sentry/vite-plugin';
import react from '@vitejs/plugin-react';
+import path from 'path';
import type {OutputOptions} from 'rollup';
import {defineConfig} from 'vite';
import jsconfigPaths from 'vite-jsconfig-paths';
@@ -164,6 +165,16 @@ export default defineConfig(({mode}) => ({
uploadToken: process.env.CODECOV_TOKEN,
}),
],
+ resolve: {
+ alias: {
+ // ensure react-router imports don't end up with multiple copies/installations. See
+ // https://github.com/remix-run/react-router/issues/12785 for more context.
+ 'react-router/dom': path.resolve(
+ './node_modules/react-router/dist/development/dom-export.mjs'
+ ),
+ 'react-router': path.resolve('./node_modules/react-router/dist/development/index.mjs'),
+ },
+ },
build: {
target: 'modules', // the default
assetsInlineLimit: 8 * 1024, // 8 KiB |
In vitest the react-router and react-router/dom imports resolve to (CommonJS?) bundles which each contain copies of the contexts/objects used by react router, which make you effectively end up with multiple installations of react router. This leads to broken contexts and failing tests, see remix-run/react-router#12785 for more context. Patching the resolution in vite config allows us to force loading the mjs modules instead of the CJS build which doesn't have this problem, because both the react-router and react-router/dom entrypoints depend on the same chunk containing the shared code.
In vitest the react-router and react-router/dom imports resolve to (CommonJS?) bundles which each contain copies of the contexts/objects used by react router, which make you effectively end up with multiple installations of react router. This leads to broken contexts and failing tests, see remix-run/react-router#12785 for more context. Patching the resolution in vite config allows us to force loading the mjs modules instead of the CJS build which doesn't have this problem, because both the react-router and react-router/dom entrypoints depend on the same chunk containing the shared code.
In vitest the react-router and react-router/dom imports resolve to (CommonJS?) bundles which each contain copies of the contexts/objects used by react router, which make you effectively end up with multiple installations of react router. This leads to broken contexts and failing tests, see remix-run/react-router#12785 for more context. Patching the resolution in vite config allows us to force loading the mjs modules instead of the CJS build which doesn't have this problem, because both the react-router and react-router/dom entrypoints depend on the same chunk containing the shared code.
This is a nice workaround for node >=22.12, but it seems to have the opposite effect, and makes it fail with older node versions. I guess it would be possible to detect the node version and add that dynamically. |
That's... Interesting since I'm on Node 20 😬 |
I found a not so terrible workaround, inspired on @sergei-maertens proposal. Since I'm only experiencing this problem when running tests with vitests, I used the aliasing resolution only inside the If you use different config files for vite and vitest, then you can use it as described there. Additionally, adding the alises fixes the problem for node >22.10, but it breaks it for older versions, where it works without the aliases. Hence, I have added a small logic to check current node version, and dynamically add the aliases only if running in node >22.10. vite.config.tsimport { defineConfig } from 'vitest/config';
+ const DEFAULT_NODE_VERSION = 'v22.10.0';
+ const nodeVersion = process.version ?? DEFAULT_NODE_VERSION;
export default defineConfig({
plugins: [/* [...] */],
// [...]
test: {
// [...]
+ alias: nodeVersion > DEFAULT_NODE_VERSION
+ ? {
+ 'react-router': resolve(__dirname, 'node_modules/react-router/dist/development/index.mjs'),
+ }
+ : undefined,
}
}); Notice the way the node version is compared ( |
If you're experiencing this issue, can you try adding the following to the Vite config you use for Vitest and report back? resolve: {
conditions: ["module-sync"],
}, |
Yep, it does fix it for node >=22.12, but it makes it fail with older versions. |
Off on a tangent, for tormented miserable souls out there who are still stuck in CRA project (like me), here's the CRA counterpart (webpack config) using module.exports = function override(config) {
const reactRouterPath = path.resolve(
__dirname,
'node_modules/react-router/dist/development/index.mjs',
);
const moduleScopePlugin = config.resolve.plugins.find(
(plugin) => plugin.constructor.name === 'ModuleScopePlugin',
);
moduleScopePlugin.allowedFiles.add(reactRouterPath);
moduleScopePlugin.allowedPaths.push(path.dirname(reactRouterPath));
config.resolve.alias = {
...config.resolve.alias,
'react-router': reactRouterPath,
};
return config;
}; |
Only on initial load; overrides the stored preferences With expansion possibility for other preference keys In the future, might want to also _set_ params with useSearchParams hook although I ran into remix-run/react-router#12785 trying that Also clean up TODO about typing of newPreferences
* Support reading showGoalNames from URL params on page load Only on initial load; overrides the stored preferences With expansion possibility for other preference keys In the future, might want to also _set_ params with useSearchParams hook although I ran into remix-run/react-router#12785 trying that Also clean up TODO about typing of newPreferences * Factor out Entries with a docstring * expand user settable values and combine searchparam and storage branches fix the record typing, so that in the typeof branches, the expected types match up * allow url param mobile override * documentation on URL parameters * support shorthand themes in urlparams
@markdalgleish I was having this issue in Vitest and adding the resolve conditions module sync block to vitest.config.ts solved it! |
Hey folks - we are fixing the bundling issue mentioned above in #13497 and that's available in an experimental release ( |
@markdalgleish Bringing the conversation from #12512 over here since I think it's the same issue. In that reproduction it's not a 3rd party lib with a peerDep that imports I did some instrumentation on my branch in the repro and was able to confirm:
The source code looks like this: import { createMemoryRouter, RouterProvider, useNavigate } from "react-router";
import { RouterProvider as RouterProviderDOM } from "react-router/dom"; And my instrumented logs come out as:
I can also confirm that adding |
How did you do this instrumentation @brophdawg11, pretty sure I tried the above and still had the issue when running the app in dev mode. |
I just added console logs to the files in |
I'm using React Router as a...
library
Reproduction
I created the minimum steps to reproduce in this repository: https://github.com/acelaya/peer-react-router-vitest
The readme includes the steps to reproduce the issue.
System Info
Used Package Manager
npm
Expected Behavior
It should be possible to depend on packages that have a peer dependency on react-router, and have no errors when running with vitest.
Actual Behavior
In short, if your project depends on react-router and a dependency which in turn has a peer-dependency on react router, when running with vitests, all imports from react-router inside that dependency will resolve a different instance than the ones in the root project, causing errors like
useLocation() may be used only in the context of a <Router> component
.This issue was reported in #12475, but it was then closed as a solution was provided that solved it for other use cases, but the error still exists when running with vitest.
It also only affects node 22.12 and newer. Earlier versions work as expected. This can be seen in this GitHub workflow execution, from the repro repository above: https://github.com/acelaya/peer-react-router-vitest/actions/runs/12864662838
The text was updated successfully, but these errors were encountered: