Have you ever updated a file in a React and Vite project, only for the entire page to reload instead of React Fast Refresh preserving your state?
That happened while I was building an authentication context. I exported both the AuthProvider component and a custom useAuth hook from the same .tsx file.
The code worked, but Vite could no longer treat the module as a reliable Fast Refresh boundary. A small refactor fixed the issue.
The Problem
My original AuthContext.tsx contained the context, provider, and custom hook:
import {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from "react";
import authApi from "@/api/auth-api";
import type { AuthContextType, AuthResponse } from "@/types/auth";
import type { User } from "@/types/users";
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({
children,
}: {
children: ReactNode;
}) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Authentication state and methods...
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 then logged the following warning:
[vite] (client) hmr invalidate /src/contexts/AuthContext.tsx
Could not Fast Refresh ("useAuth" export is incompatible).
Vite’s HMR system was still working, but React Fast Refresh could not safely preserve the component state for this module.
The provider is a React component, while useAuth is a hook. Exporting both from the same component module caused Vite to invalidate the module and propagate the update, which could result in a full-page reload.
The Fix
The fix was to separate the context and hook from the provider component.
AuthContext.ts
import {
createContext,
useContext,
} from "react";
import type { AuthContextType } from "@/types/auth";
export const AuthContext =
createContext<AuthContextType | undefined>(
undefined
);
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error(
"useAuth must be used within an AuthProvider"
);
}
return context;
}
AuthProvider.tsx
import {
useEffect,
useState,
type ReactNode,
} from "react";
import authApi from "@/api/auth-api";
import { AuthContext } from "@/contexts/AuthContext";
import type { AuthResponse } from "@/types/auth";
import type { User } from "@/types/users";
export function AuthProvider({
children,
}: {
children: ReactNode;
}) {
const [user, setUser] = useState<User | null>(
null
);
const [isLoading, setIsLoading] =
useState(true);
// Authentication state and methods...
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
After splitting the files, the Fast Refresh warning disappeared and updates no longer caused the entire page to reload.
Why This Works
React Fast Refresh works best when modules exporting React components remain consistent component boundaries.
Moving the context and hook into a separate .ts module leaves AuthProvider.tsx focused on exporting the provider component.
This is not a strict rule that every .tsx file can only contain component code, but it is a useful structure for avoiding incompatible Fast Refresh exports.
The ESLint rule below can also detect this pattern:
react-refresh/only-export-components
Takeaway
When a React and Vite project unexpectedly performs full-page reloads, check the exports in the affected component module.
A practical structure is:
contexts/
├── AuthContext.ts
└── AuthProvider.tsx
Keep component modules focused on component exports, and move reusable hooks, contexts, and utilities into separate files where appropriate.
The application behaviour may already work correctly, but separating the modules can restore Fast Refresh and preserve local state during development.
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.tsfile.I didn't want to name the file
Auth.tssince 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!