DEV Community

Cover image for Fixing React Fast Refresh Issues in Vite by Splitting Your Auth Context
Terry W
Terry W

Posted on • Edited on

Fixing React Fast Refresh Issues in Vite by Splitting Your Auth Context

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;
}
Enter fullscreen mode Exit fullscreen mode

Vite then logged the following warning:

[vite] (client) hmr invalidate /src/contexts/AuthContext.tsx
Could not Fast Refresh ("useAuth" export is incompatible).
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
rickyryden profile image
Ricky Rydén • Edited

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.

- /hooks/useAuth.ts
- /contexts/authContext.ts
- /providers/AuthProvider.tsx
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ttibbs profile image
Terry W

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

Collapse
 
zoltan438973634 profile image
Zoltan • Edited

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!! 🙏

Collapse
 
ttibbs profile image
Terry W

No problem at all. I'm glad it was useful!