-
-
Notifications
You must be signed in to change notification settings - Fork 6.3k
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
HMR breaks when modifying React context provider #3301
Comments
In my case the context mechanism behind react seems to break - I get 2 renders of the same component:
My config for vite is empty so no other plugins can be influencing this. The only thing that helps is restarting vite (simple reload of the website does not work so it must be caching of HMR even on reloads). Disabling HMR entirely After changing more things afterwards I suddenly started getting a weird
Debug trace from Vite$ vite --debug vite:config TS + native esm config loaded in 27ms URL { href: 'file:///home/some-random-app/packages/app/vite.config.ts', origin: 'null', protocol: 'file:', username: '', password: '', host: '', hostname: '', port: '', pathname: '/home/some-random-app/packages/app/vite.config.ts', search: '', searchParams: URLSearchParams {}, hash: '' } +0ms vite:config using resolved config: { vite:config server: { fs: { strict: undefined, allow: [Array] } }, vite:config configFile: '/home/some-random-app/packages/app/vite.config.ts', vite:config configFileDependencies: [ 'vite.config.ts' ], vite:config inlineConfig: { vite:config root: undefined, vite:config base: undefined, vite:config mode: undefined, vite:config configFile: undefined, vite:config logLevel: undefined, vite:config clearScreen: undefined, vite:config server: { fs: [Object] } vite:config }, vite:config root: '/home/some-random-app/packages/app', vite:config base: '/', vite:config resolve: { dedupe: undefined, alias: [ [Object], [Object] ] }, vite:config publicDir: '/home/some-random-app/packages/app/public', vite:config cacheDir: '/home/some-random-app/packages/app/node_modules/.vite', vite:config command: 'serve', vite:config mode: 'development', vite:config isProduction: false, vite:config plugins: [ vite:config 'vite:pre-alias', vite:config 'alias', vite:config 'vite:modulepreload-polyfill', vite:config 'vite:resolve', vite:config 'vite:html', vite:config 'vite:css', vite:config 'vite:esbuild', vite:config 'vite:json', vite:config 'vite:wasm', vite:config 'vite:worker', vite:config 'vite:asset', vite:config 'vite:define', vite:config 'vite:css-post', vite:config 'vite:client-inject', vite:config 'vite:import-analysis' vite:config ], vite:config build: { vite:config target: [ 'es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1' ], vite:config polyfillModulePreload: true, vite:config outDir: 'dist', vite:config assetsDir: 'assets', vite:config assetsInlineLimit: 4096, vite:config cssCodeSplit: true, vite:config sourcemap: false, vite:config rollupOptions: {}, vite:config commonjsOptions: { include: [Array], extensions: [Array] }, vite:config dynamicImportVarsOptions: { warnOnError: true, exclude: [Array] }, vite:config minify: 'terser', vite:config terserOptions: {}, vite:config write: true, vite:config emptyOutDir: null, vite:config manifest: false, vite:config lib: false, vite:config ssr: false, vite:config ssrManifest: false, vite:config brotliSize: true, vite:config chunkSizeWarningLimit: 500, vite:config watch: null vite:config }, vite:config env: { vite:config VITE_FUZZ_LOCALES: 'true', vite:config BASE_URL: '/', vite:config MODE: 'development', vite:config DEV: true, vite:config PROD: false vite:config }, vite:config assetsInclude: [Function: assetsInclude], vite:config logger: { vite:config hasWarned: false, vite:config info: [Function: info], vite:config warn: [Function: warn], vite:config warnOnce: [Function: warnOnce], vite:config error: [Function: error], vite:config clearScreen: [Function: clearScreen], vite:config hasErrorLogged: [Function: hasErrorLogged] vite:config }, vite:config createResolver: [Function: createResolver], vite:config optimizeDeps: { esbuildOptions: { keepNames: undefined } } vite:config } +5ms vite:deps Hash is consistent. Skipping. Use --force to override. +0ms
Stacktraces of the 2 context calls done in the component
|
I am also facing the same issue and indeed it's related to Vite or @vitejs/plugin-react. I have tried to run the same application using the react-scripts package (used by create-react-app) and I was able to preserve the state even after modifying the context provider. Wondering if it's because Vite HMR triggers component reload (resetting any previous state) on modifying initial state values: // CounterContext.jsx
const CounterContext = createContext({
counter: 0,
setCounter: (value) => { console.log('this is default value') }
})
/* CouterProvider ... */
// MyComponent.jsx
const MyComponent = (props) => {
const { counter, setCounter } = useContext(CounterContext);
/* Render logic ... */
} When CounterContext.jsx is modified, the counter and setCounter inside MyComponent.jsx are assigned the default context values and you can see it printing "this is default value" on calling setCounter. |
@selrond Is there a solution to this problem? |
While attempting to migrate an app that uses context quite a bit, this has blocked me from moving forward as well. It would be great to get an update on whether or not this is going to be worked on. It seems to happen when the createContext hook is in the same file as the component where the Provider is used. In my case, I don't have a clean way to insert it on a parent component without wrapping my entire app where the routes are, which is not something I wish to do. I found a related issue in Snowpack, which also uses |
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
Same issue here, but im trying to be more specific with that. In my experience:
export const Context = createContext();
export const Provider = ({children}) => (
<Context.Provider value={/* whatever*/}>{children}</Context.Provider>
) Running
export const Context = createContext(); import { Context } from './my-context-file';
export const Provider = ({children}) => (
<Context.Provider value={/* whatever*/}>{children}</Context.Provider>
) Running Tell me if you need more info, or a minium reproduction repo. Hope it helps! |
I'm also stuck with issue related to Context and HMR/fast refresh. To workaround this issue, I just use |
In my case, I was trying to add a MobX store with default provider: export class Store implements StoreSchema {
commonStore = new CommonStore();
userStore = new UserStore();
}
export const store = new Store();
export const StoreContext = createContext(store);
export const useStore = () => {
return useContext(StoreContext);
}; then calling it in the ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<StoreContext.Provider value={store}> {/* */}
<CustomRouter history={history}>
<App />
</CustomRouter>
</StoreContext.Provider>
</React.StrictMode>
); It works at first, but after any change, (whether in context, component, style, etc.), and hot reload, I get a white screen with this error in console:
Update - Creating context and provider in one file partially solves the problem. However, still editing inside the context itself break the code and require page refresh. Here is what I tried: interface StoreSchema {
commonStore: CommonStore;
userStore: UserStore;
}
export class Store implements StoreSchema {
commonStore = new CommonStore();
userStore = new UserStore();
}
export const store = new Store();
const storeContext = createContext(store);
export const StoreContextProvider = (props: { children: ReactNode | JSX.Element }) => {
return <storeContext.Provider value={store}>{props.children}</storeContext.Provider>;
};
export const useStore = () => {
return useContext(storeContext);
}; and only call the let reactRoot;
// to avoid react console warning: root is already defined.
if (!reactRoot) {
const root = document.getElementById("root");
if (root) { // to avoid eslint warning on using non-null assertion
reactRoot = ReactDOM.createRoot(root);
} else {
console.error("No root element is defined in the index file.");
}
}
reactRoot?.render(
<React.StrictMode>
<StoreContextProvider>
<CustomRouter history={history}>
<App />
</CustomRouter>
</StoreContextProvider>
</React.StrictMode>
); |
I tried the 2nd option, though it does work with HMR. We kind-a lose the capability of Fast Refresh. While fast refresh is still buggy, I think disabling it for now is the only workaround |
I finally find a trick to make it works => implement my own vite plugin which automatically wraps |
@stregouet This sounds really promising - would you be able to share your plugin? I have a repo that has turned into a bit of a nightmare to work on because of this issue. |
@kalda341 I didn't publish it anywhere because it is not really robust and a bit too specific to my case. anyway here is the code: import type { Plugin, ResolvedConfig } from "vite";
export default function cacheContext() {
let config: ResolvedConfig;
const p: Plugin = {
name: "myorg:cache-context",
configResolved(resolvedConfig) {
// store the resolved config
config = resolvedConfig;
},
transform(src, id) {
if (config.command !== "serve") {
// not in dev mode
return;
}
// this if allow me to speed up plugin process, since all of my context
// are located in `context` directory
if (!/mypkg\/src\/context/.test(id)) {
return;
}
return {
code:
`
window.contextCache = window.contextCache || {
cache: new Map(),
get(key, inst) {
if (this.cache.has(key)) {
return this.cache.get(key);
}
this.cache.set(key, inst);
return inst;
}
};
` +
src.replace(
/export const ([^=]*)=\s*React.createContext\(([^;]*);/g,
(_, name: string, value: string) =>
`export const ${name} = ` +
`window.contextCache.get("${name.trim()}", React.createContext(${value});`,
),
map: null,
};
},
};
return p;
} you see, it is regex based, but to manipulate code it is better and more robust to use something like babel (as jotai did) |
same issue here, export useContext will be break when hot reload... feeling weird, and im still trying figure out the solution...
i tried this, wont work... I still had to manually refresh everytime when i save work on my vite project tho. Could anyone tell me which old version did not have this problem, i wish to downgrade temporary to continue work with my project smoothly...
@sodatea can we mark it as major bug? I found an temporary solution which is downgrade the vite version to v2.5.0 and use |
Vite 3 is out, but React context still do not support HMR |
Some correction, Vite 3 is out, however Vite 3's HMR still didn't support React Context yet. Your welcome! Hence I would not recommend react developers using vite as development tool. Hence, u see Nextjs, ionic, and bunch of huge react frameworks still did not using vite yet. Nextjs, Webpack are much more mature for react development, however vite is more focus on vue 3 based on my understanding 👍 Some add on, React Context is great feature, without React context, We cant build a scalable and huge react project with ease! For those thumb down reaction, please share your thought and tell me am i say something wrong? Please correct me if im wrong, Thanks! |
I have this same issue. Whenever I modify the context, I usually have to a manual page reload to fix it. I thought it'd be cool if vite had a feature where I could add a comment to the top of a file like I could put at the top of a react context file to do page reload instead of HMR when I edit that file. |
@seeker-3 I believe you could use the HMR API to achieve the same: https://vitejs.dev/guide/api-hmr.html#hot-decline |
We have been using CRA with typescript from version 2.X to 4.X. In the span of three years our codebase grew a lot. Starting our application locally takes more than a minute. Building it is even slower in CI, more than 15 minutes. Hot reloading also took more than five seconds. Migrating to Nextjs would require a lot of refactorings on our end since we heavily rely on React context as global state management. And it's routing is based on file system which makes it complicated to migrate our app. After migrating CRA to Vite we've managed to speed up starting our application from more than a minute to instantaneous (less than a second). And the build took less than 9 minutes (more than 15 minutes before). |
weird tho, vite does not support react context when hot reloading, how could you development your app, manually refreshing the page/app every time after hot reload? dont you feel troublesome when developing react app with this behavior? |
Ours automatically reloads whenever a file has been saved. There are a lot of guides roaming around. Like this one: https://www.darraghoriordan.com/2021/05/16/migrating-from-create-react-app-to-vite/ Though we have a lot of dependencies issues but we managed to resolve them all. |
This is the kind of edge case behaviour that would probably affect very few people but those affected would likely be driven to the brink of insanity before they figured out what was causing it 😅 |
Agreed. 🙂 I think most here would love this plugin once its written, because it's basically plug-and-play and handles 99% of use cases. |
@justinbhopper It works so smooth with no issues. Here's the steps: 1- create a new vite react ts project.
3- Inside vite.config.ts, add this plugin:
And it works. Now we just need some better regex matching rules. Update 1: Tried this with context in a separated file, it doesn't work. |
Yes, it's possble. For now, I wanted to avoid caveats while using the plugin. The best way is to write a babel transform, but it requires some work and I wanted to check if this works for people.
Do you have a reproduction of this one?
I didn't think of that, but it seems it's working well? (stackblitz) |
@sapphi-red your method works perfectly well, with nested level, separated files and everything. |
Worked like a charm for our usecase on a medium-large project. |
Can someone confirm this? I just ran Then tried to edit the context, counter, app, etc. and all worked with no issues. |
@adnanalbeda I also cannot reproduce the bug using the original repo (https://github.com/selrond/vite-react-usecontext) after upgrading it to |
Great, so the issue is resolved, and no one knows why. |
It still happens for me. |
@sapphi-red , are you trying it locally? or just on stackblitz? |
I was trying on stackblitz. But I tried it now with local and it happened. |
Thanks, now I know what's going on. DON'T define the context with its provider in the SAME FILE. HMR will break if any change happen in that file, or any file that is used in the context or the provider. More about it. If we have one file with this code: import React from 'react';
export const CountContext =
// __preserveRef("CountContext", React.createContext<any>(null));
React.createContext<any>(null)
;
export const CountContextProvider = (props: { children: React.ReactNode }) => {
const [count, setCount] = React.useState(1);
return (<CountContext.Provider value={{ count, setCount }}>
{props.children}
</CountContext.Provider>)
} Any change here, HMR will break. But separate them into two files: // CountContext(.ts or .tsx)
import React from 'react';
export const CountContext =
// __preserveRef("CountContext", React.createContext<any>(null));
React.createContext<any>(null)
; and // CountContext.tsx
import React from "react";
import { CountContext } from "./CountContext";
export const CountContextProvider = (props: { children: React.ReactNode }) => {
const [count, setCount] = React.useState(1);
return (<CountContext.Provider value={{ count, setCount }}>
{props.children}
</CountContext.Provider>)
} In this way, the issue is resolved, and changes can happen safely everywhere. I will test it next with a simple mobx store and see what happens. Update: After adding a small mobx store, I can confirm, this issue only happens when both context and provider are defined in the same file. |
The same issue with |
Just ran into this issue on a non-vite project. We are introducing context for the first time into our app. After messing around I finally found that yes moving the createContext call to another file resolves the issue mostly. Editing that file still causes the problem. |
FYI, we stumbled over this problem too, and are using a (somewhat hacky) solution that seems to work, without needing to write Vite plugins or fiddling with HMR API. The trick is to move the "createContext" call into the provider component. Using a React ref ensures uniqueness. What do you think? Does this solution have any downsides I am not aware of (except that this has to be repeated for every context, and goes into production code)? import { createContext, useContext, useRef } from "react";
let MyContext;
export function MyProvider({ children }) {
const ref = useRef();
MyContext = ref.current ??= createContext();
...
return <MyContext.Provider value={...}>{children}</MyContext.Provider>;
}
export default function useMyContext() {
return useContext(MyContext);
} |
This might be related to vite performing HMR too eagerly: #9869. |
I noticed the 54 likes, the 47 comments, a few duplicate issues, and the implementation of The issue doesn't feel it deserves the |
It's still a strong reason to choose nextjs instead of vitejs just because of context losing, in real app, a context losing will easily cause a tab 100% CPU because of an error loop, have to keep a task manager open and kill the tab. |
Not sure why people are thumbs downing @wenerme - I'm working on a very large app where these HMR issues are leading to all kinds of unpredictable issues. In some cases this does lead to having to kill the tab, in some cases it leads to unexpected app behaviors, and in some cases an Auth0 redirect loop (which is so hard to debug that I'm not even 100% sure it's related to this issue). It really is a massive headache for me and my team, though the size of the app means that disabling fastRefresh is also not a great solution. |
I've pushed up a PR that fixes this issue, according to a test I wrote based off the reproduction in the issue description. But, it requires a change to how Vite handles |
Describe the bug
Vite HMR breaks when modifying React context provider
Related: vitejs/vite-plugin-react#24
Reproduction
selrond/vite-react-usecontext
System Info
Output of
npx envinfo --system --npmPackages vite,@vitejs/plugin-vue --binaries --browsers
:Used package manager: npm
Logs
Before submitting the issue, please make sure you do the following
The text was updated successfully, but these errors were encountered: