Have you ever updated a file in your React + Vite project only to see full page reloads instead of fast refresh? 😩
That happened to me when I built an authentication context and exported both the provider and a custom hook (useAuth
) from the same .tsx
file. Turns out, that’s a no-go if you want smooth Hot Module Reloading (HMR) with Vite.
Here’s what was happening and how a small refactor completely fixed it.
The Problem
I had a single AuthContext.tsx
file that looked something like this:
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";
import authApi from "@/api/auth-api";
import { User } from "@/types/users";
import { AuthContextType, AuthResponse } from "@/types/auth";
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
...
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
Vite kept logging:
vite hmr invalidate /src/contexts/AuthContext.tsx Could not Fast Refresh ("useAuth" export is incompatible). Learn more at https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#consistent-components-exports
Basically, Vite's React plugin only supports Fast Refresh when your .tsx file exports components only (i.e., PascalCase components). If you export anything else — like a custom hook — it invalidates the module and reloads the whole page.
The Fix
The cleanest solution: split your hook into a separate .ts file.
AuthContext.ts
import { createContext, useContext } from "react";
import type { AuthContextType } from "@/types/auth";
export const AuthContext = createContext<AuthContextType | undefined>(
undefined
);
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within <AuthProvider>");
return ctx;
}
AuthProvider.tsx
import { useState, useEffect, ReactNode } from "react";
import authApi from "@/api/auth-api";
import { User } from "@/types/users";
import { AuthResponse } from "@/types/auth";
import { AuthContext } from "@/contexts/AuthContext";
export function AuthProvider({ children }: { children: ReactNode }) {
// ... state + auth methods here ...
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
Takeaway
If you're using Vite with React and notice weird HMR behaviour, check your exports. Keep .tsx files component-only, and move hooks or utilities into .ts files to avoid these issues.
Have you run into other quirky Vite or React dev issues? Got any lightweight performance tricks? Drop them in the comments, and let others know what’s worked for you! 👇
Top comments (4)
I was struggling with this and I could not understand why I still got the error, even after I moved my custom hooks to a
/hooks/auth.ts
file.I didn't want to name the file
Auth.ts
since it was not a component, and it turns out that the error only occurred when a file starts with lower case.So I moved all my hooks to their own files (starting with
use
, for example:useAuth.ts
), and that works without any errors.I use this structure, similar code like in your post, but I chose to place hook and context in different files.
Thank you for pointing that out, it's something I overlooked when creating the post. But generally, yes, this is a much better way to structure your folders and files
OMG I've been struggling for days with this, it's been driving me crazy. I've tried so many things, this is the only one that actually worked. Thank you so much!! 🙏
No problem at all. I'm glad it was useful!