What's the Alternative to Using React Hooks in Non-React Components? Accessing useAuth Data Without Manual Token Passing

React Hooks (like useState, useContext, or custom hooks such as useAuth) have revolutionized state management in React components, offering a clean, functional way to handle side effects, context, and reusable logic. However, a common pain point arises when non-React code (e.g., utility functions, API clients, class components, or third-party libraries) needs access to data managed by hooks—such as authentication tokens, user sessions, or user roles.

Hooks are designed to work only within React function components or custom hooks (per React’s Rules of Hooks). This restriction leaves developers scratching their heads: How do I access useAuth data (like a JWT token) in a utility function that makes API calls, without manually passing the token everywhere?

Manual token passing—where components fetch the token via useAuth and explicitly pass it to non-React code—quickly becomes cumbersome. It leads to prop drilling, tight coupling between components and utilities, and harder-to-maintain code.

In this blog, we’ll explore practical alternatives to using React Hooks in non-React components, focusing on accessing authentication data seamlessly. We’ll compare solutions like global state stores, context accessors, state management libraries, and dependency injection, so you can choose the best approach for your app.

Table of Contents#

  1. Understanding the Limitation: Why Hooks Can’t Be Used Outside React Components
  2. The Problem with Manual Token Passing
  3. Alternatives to Access useAuth Data in Non-React Components
  4. Comparison of Alternatives
  5. Conclusion
  6. References

1. Understanding the Limitation: Why Hooks Can’t Be Used Outside React Components#

React Hooks are powerful, but they come with strict rules. The first rule is: Hooks can only be called inside React function components or custom Hooks (React Docs). This is because Hooks rely on React’s internal state management system, which tracks component renders and Hook calls in a specific order.

For example, if you try to call useAuth() (a custom hook that returns the current user/token) in a utility function like apiClient.js, React will throw an error:

// apiClient.js (Non-React utility)
import { useAuth } from './useAuth';
 
export const fetchData = () => {
  const { token } = useAuth(); // ❌ Error: Invalid hook call
  return fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
};

This error occurs because fetchData is not a React component or custom hook, so React can’t track the Hook call. Thus, we need alternative ways to access useAuth data in non-React code.

2. The Problem with Manual Token Passing#

A common workaround is to manually pass the token from React components to non-React code. For example:

// Component using fetchData
import { useAuth } from './useAuth';
import { fetchData } from './apiClient';
 
const MyComponent = () => {
  const { token } = useAuth(); 
  const data = fetchData(token); // Pass token manually
  // ...
};
 
// apiClient.js (now accepts token as argument)
export const fetchData = (token) => {
  return fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
};

While this works, it has critical drawbacks:

  • Prop Drilling: If a utility is used deep in the component tree, the token must be passed through multiple levels of components.
  • Tight Coupling: Non-React code (e.g., apiClient.js) becomes dependent on components to provide the token, making it harder to reuse or test independently.
  • Maintenance Overhead: Changing how auth data is accessed (e.g., switching from token to sessionId) requires updates in every component passing the value.

3. Alternatives to Access useAuth Data in Non-React Components#

Let’s explore robust alternatives to manual token passing, each with trade-offs to consider.

3.1 Global Auth Store (Module Singleton)#

A global auth store is a standalone module that holds auth state (e.g., token, user) and exposes methods to read or update it. React components update this store when auth changes (e.g., on login/logout), and non-React code imports the store to access the data.

How It Works:#

  1. Create a singleton module (e.g., authStore.js) with:

    • State variables (e.g., token, user).
    • Methods to update state (setToken, clearToken).
    • Methods to read state (getToken, getUser).
  2. In React components, use useAuth to get the latest auth data and sync it with the global store.

  3. Non-React code imports authStore and calls getToken() to access the token.

Example Implementation:#

Step 1: Define the global store (authStore.js)

// authStore.js (Singleton module)
let _token = null;
let _user = null;
 
export const authStore = {
  getToken: () => _token,
  getUser: () => _user,
  setAuth: (token, user) => {
    _token = token;
    _user = user;
  },
  clearAuth: () => {
    _token = null;
    _user = null;
  },
};

Step 2: Sync React components with the store

// AuthProvider.js (React component to sync useAuth with authStore)
import { useAuth } from './useAuth';
import { authStore } from './authStore';
import { useEffect } from 'react';
 
export const AuthStoreSync = () => {
  const { token, user, isAuthenticated } = useAuth(); // From your existing useAuth hook
 
  useEffect(() => {
    if (isAuthenticated) {
      authStore.setAuth(token, user); // Update store on auth change
    } else {
      authStore.clearAuth(); // Clear on logout
    }
  }, [token, user, isAuthenticated]);
 
  return null; // This component doesn't render anything
};

Step 3: Use the store in non-React code

// apiClient.js (Non-React utility)
import { authStore } from './authStore';
 
export const fetchData = () => {
  const token = authStore.getToken(); // Access token from global store
  if (!token) throw new Error('No token available');
  return fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
};

Step 4: Wrap your app with the sync component

// App.js
import { AuthProvider } from './AuthProvider'; // Your existing auth context/provider
import { AuthStoreSync } from './AuthStoreSync';
 
const App = () => {
  return (
    <AuthProvider>
      <AuthStoreSync /> {/* Syncs auth state to the global store */}
      {/* Rest of your app */}
    </AuthProvider>
  );
};

Pros:#

  • Simplicity: No external libraries; just a plain JavaScript module.
  • Decoupling: Non-React code imports the store directly, no need for manual passing.

Cons:#

  • Stale Data Risk: The store is not reactive. If the token updates, non-React code won’t automatically receive the new value unless it re-fetches getToken().
  • No Subscriptions: Non-React code can’t “subscribe” to auth changes (e.g., to refresh a token when it expires).

3.2 Context Provider with a Separate Accessor#

React Context is designed to share state across components, but it’s typically consumed via useContext in components. To access context outside components, we can use a ref to track the current context value, updated whenever the context changes.

How It Works:#

  1. Create an Auth Context to hold auth state.
  2. Use a ref in the Context Provider to store the latest context value.
  3. Expose a function (e.g., getAuth()) that returns the ref’s current value, allowing access outside components.

Example Implementation:#

Step 1: Define the Context and ref accessor

// AuthContext.js
import { createContext, useContext, useRef, useEffect } from 'react';
 
// Create Context
const AuthContext = createContext(null);
 
// Ref to hold the latest context value
let authContextRef = null;
 
// Accessor function for non-React code
export const getAuth = () => {
  if (!authContextRef) {
    throw new Error('AuthContext not initialized. Wrap your app with AuthProvider.');
  }
  return authContextRef.current;
};
 
// Provider component
export const AuthProvider = ({ children, value }) => {
  const ref = useRef(null);
 
  // Update ref whenever context value changes
  useEffect(() => {
    ref.current = value;
    authContextRef = ref; // Expose ref globally
  }, [value]);
 
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
 
// Custom hook for React components (standard usage)
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

Step 2: Wrap your app with the Provider

// App.js
import { AuthProvider } from './AuthContext';
import { useAuth } from './useAuth'; // Your existing auth logic (e.g., from Firebase, Auth0)
 
const RootAuthProvider = ({ children }) => {
  const auth = useAuth(); // Get auth state from your existing hook (e.g., Firebase's useAuth)
  return <AuthProvider value={auth}>{children}</AuthProvider>;
};
 
const App = () => {
  return (
    <RootAuthProvider>
      {/* Rest of your app */}
    </RootAuthProvider>
  );
};

Step 3: Use getAuth() in non-React code

// apiClient.js (Non-React utility)
import { getAuth } from './AuthContext';
 
export const fetchData = () => {
  const { token } = getAuth(); // Access context via the ref
  return fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
};

Pros:#

  • Reactive Updates: The ref updates whenever the context value changes, so getAuth() returns fresh data.
  • No Manual Sync: Unlike the singleton store, there’s no need for a separate sync component.

Cons:#

  • SSR Risks: On the server, authContextRef may not be initialized, leading to errors. Use caution with server-side rendering (SSR).
  • Tight Coupling to React: Relies on React’s context and ref system, making it less portable outside React apps.

3.3 State Management Libraries (Zustand, Redux, Jotai)#

State management libraries like Zustand, Redux, or Jotai are designed to manage global state outside React components. They expose APIs to access state from anywhere, including non-React code.

Example with Zustand:#

Zustand is lightweight and requires minimal setup. It lets you create a store that can be imported and used in both React and non-React code.

Step 1: Define the auth store with Zustand

// authStore.js (Zustand store)
import { create } from 'zustand';
 
// Create a store with auth state and actions
export const useAuthStore = create((set) => ({
  token: null,
  user: null,
  setAuth: (token, user) => set({ token, user }),
  clearAuth: () => set({ token: null, user: null }),
}));

Step 2: Sync React auth state with the store
If your app already uses a React-based auth solution (e.g., Firebase, Auth0), sync its state with the Zustand store:

// AuthSync.js (React component)
import { useAuth } from './useAuth'; // Your existing React auth hook
import { useAuthStore } from './authStore';
 
export const AuthSync = () => {
  const { token, user, isAuthenticated } = useAuth(); // From your auth provider
  const setAuth = useAuthStore((state) => state.setAuth);
  const clearAuth = useAuthStore((state) => state.clearAuth);
 
  useEffect(() => {
    if (isAuthenticated) {
      setAuth(token, user);
    } else {
      clearAuth();
    }
  }, [token, user, isAuthenticated, setAuth, clearAuth]);
 
  return null;
};

Step 3: Access the store in non-React code
Zustand stores expose a getState() method to access state outside React:

// apiClient.js (Non-React utility)
import { useAuthStore } from './authStore';
 
export const fetchData = () => {
  const { token } = useAuthStore.getState(); // Access state via getState()
  return fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
};

Pros:#

  • Reactive and Predictable: Zustand stores are reactive—non-React code can subscribe to state changes (if needed) using subscribe().
  • SSR Support: Zustand works with server-side rendering (unlike the context ref approach).
  • Rich Ecosystem: Extensible with middleware (e.g., persistence, dev tools).

Cons:#

  • Dependency: Adds a third-party library (though Zustand is tiny, ~1KB).

3.4 Dependency Injection#

Dependency injection (DI) is a design pattern where non-React code receives dependencies (e.g., auth data) from external sources rather than fetching them directly. This decouples the code from how auth data is retrieved.

How It Works:#

  1. Define non-React code (e.g., apiClient.js) to accept auth data as a parameter or option.
  2. Use a factory or higher-order function to inject the auth data into the utility when it’s created.

Example Implementation:#

Step 1: Define the utility with dependency injection

// apiClient.js (DI-friendly utility)
export const createAPIClient = (getToken) => {
  return {
    fetchData: () => {
      const token = getToken(); // Token is injected via getToken()
      return fetch('/api/data', { headers: { Authorization: `Bearer ${token}` } });
    },
  };
};

Step 2: Inject the token in React components
Components using the API client inject the token via a getToken function (e.g., from useAuth):

// DataComponent.jsx (React component)
import { useAuth } from './useAuth';
import { createAPIClient } from './apiClient';
 
const DataComponent = () => {
  const { token } = useAuth();
  // Inject getToken into the API client
  const apiClient = createAPIClient(() => token);
 
  const fetchData = async () => {
    const data = await apiClient.fetchData(); // No manual token passing!
    // ...
  };
 
  return <button onClick={fetchData}>Fetch Data</button>;
};

Pros:#

  • Testability: Easy to mock getToken() in tests (e.g., pass a function that returns a test token).
  • Flexibility: Works with any auth system—just change the getToken implementation.

Cons:#

  • Still Requires Component Involvement: Components must still inject the dependency, though it’s centralized in the factory.

4. Comparison of Alternatives#

AlternativeSetup ComplexityReactive?External DependenciesSSR SupportBest For
Global Auth Store (Singleton)LowNoNoneGoodSmall apps; simple use cases.
Context + Ref AccessorMediumYesNoneCautiousApps already using React Context.
Zustand/State ManagementLow-MediumYesZustand/Redux/JotaiGoodLarger apps; need for reactivity/subscriptions.
Dependency InjectionMediumYes (via DI)NoneGoodTestable code; decoupled utilities.

5. Conclusion#

Accessing useAuth data in non-React components without manual token passing is achievable with the right strategy:

  • For small apps: Use a global singleton store for simplicity.
  • For React Context users: Use a context ref accessor to bridge React and non-React code.
  • For larger apps: Adopt a state management library like Zustand for reactivity and scalability.
  • For testability: Use dependency injection to decouple utilities from auth data sources.

Choose the approach that aligns with your app’s size, existing architecture, and needs for reactivity or testability.

6. References#