Search code examples
reactjsreact-hooksspring-securityjwt

getting React Hook "useSetupInterceptors" cannot be called at the top level when try to use useSignOut hook


I am trying to learn react js and springboot application, and when java restart i am invalidating a tokenVersion (timestamp in jwt payload but not actual token) in jwt token.

when java restart and if i try to reload react page or navigating spring security throws 403 forbidden.

using the response i am checking and try to force the user to logout and try to use signOut hook to cleanup the state or session but i am getting the react hook error.

I tried many ways from the chat gpt but none of them give the solution.

these are my following react codes:

1. EndpointConfig.js

import axios from 'axios';
import useSetupInterceptors from '../util/useSetupInterceptors'
const config = window.APP_CONFIG;
const endPointURLIP = config.endPointIP;
const defaultUrl = "localhost";
if(!endPointURLIP)
  endPointURLIP = defaultUrl;

  //Retrieving Token from the cookie which saved while login.
  const getTokenFromCookie = () => {
      const cookies = document.cookie.split(';').map(cookie => cookie.trim());
      for (const cookie of cookies) {
          if (cookie.startsWith('_auth=')) {
              const tokenCookie = cookie.split('=');
              if (tokenCookie.length === 2) {
                  return tokenCookie[1];
              }
          }
      }

      return null; // Token not found in cookies
  };
  const authToken = getTokenFromCookie();

  const getAxiosHeader = () => {
    const API_BASE_URL = 'http://'+endPointURLIP+':9081';
    const AUTHENTICATION_HEADER_TOKEN = authToken ? `Bearer ${authToken}` : null;

    return axios.create({
      baseURL: API_BASE_URL,
      headers: {
        Authorization: AUTHENTICATION_HEADER_TOKEN,
      },
    });
  };
  const authAxiosHeader = getAxiosHeader();
  useSetupInterceptors(authAxiosHeader);


  const endpointConfig = {
    a: 'http://'+endPointURLIP+':9081',
     HISTORY_ENDPOINT: '/demo/history',

    AUTHENTICATION_HEADER_TOKEN: authToken ? `Bearer ${authToken}` : null,
    AUTHENTICATION_HEADER:authAxiosHeader,
  };

export default endpointConfig;

2. useSetupInterceptors.js

import { useEffect } from 'react';
import { useSignOut } from 'react-auth-kit';

const useSetupInterceptors = ({ axiosInstance }) => {
  const signOut = useSignOut();
  useEffect(() => {
    const interceptor = axiosInstance.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response && error.response.status === 403) {
          alert('Request failed with status code 403:', error.message);
          handleLogout();
        }
        return Promise.reject(error);
      }
    );


  const handleLogout = () => {
      window.location.href = '/';
      signOut();
  };
 return () => {
 axiosInstance.interceptors.response.eject(interceptor);
 };
 }, [axiosInstance, signOut]);

};

export default useSetupInterceptors;

3. DemoService.js

import axios from 'axios';
import endpointConfig from './EndpointConfig';

class DemoService {

  constructor() {
    this.baseUrl = endpointConfig.API_BASE_URL;
    this.getHistoryEndpoint = endpointConfig.HISTORY_ENDPOINT;
    this.axiosHeader = endpointConfig.AUTHENTICATION_HEADER;
  }
  async getDemoHistory() {
    try {
      const response = await this.axiosHeader.get(`${this.getHistoryEndpoint}`);
      const configData = response.data;
      return configData;
    } catch (error) {
      console.log('Error retrieving data:'+error);
    }
  }
}
export default new DemoService();

DemoHistory.js

import React, { Component } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import DemoService from '../services/DemoService';

class DemoHistory extends Component {
  constructor(props) {
    super(props);
    this.state = {
      resultFromController: [],
      isLoading: true,
    };
  }

  componentDidMount() {
    this.getDemoHistory();
  }

  getDemoHistory = () => {
    DemoService.getDemoHistory()
      .then((res) => {
        this.setState({ resultFromController: res, isLoading: false });
      })
      .catch((error) => {
        console.error('Error fetching Demo history:', error);
        this.setState({ isLoading: false });
      });
  };

  render() {
    const { resultFromController, isLoading } = this.state;

    if (isLoading) {
      return <div>Loading...</div>;
    }

    return (
      <div>
        <h1>Demo History</h1>
        <DataTable value={resultFromController}>
          <Column field="fromUser" header="From User" />
          <Column field="toUser" header="To User" />
        </DataTable>
      </div>
    );
  }
}

export default DemoHistory;

App.js

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { AuthProvider, RequireAuth } from 'react-auth-kit';
import DemoHistory from './components/DemoHistory';

function App() {
  return (
    <AuthProvider
      authType={"cookie"}
      authName={"_auth"}
      cookieDomain={window.location.hostname}
      cookieSecure={false}
    >
      <div>
        <h2 className="header_name">Demo</h2>
        <BrowserRouter>
          <Routes>
            <Route path="DemoHistory" element={<RequireAuth loginPath="/"><DemoHistory /></RequireAuth>} />
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

Login.js

import React, { useState } from "react";
import { Button, Form } from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import { useNavigate } from "react-router-dom";
import { useAuthHeader, useSignIn ,useSignOut } from "react-auth-kit";
import LoginService from "../services/LoginService";

export default function Login() {
  const [loginId, setLoginId] = useState("");
  const [password, setPassword] = useState("");
  const loginService = new LoginService();
  const navigate = useNavigate();
  const { setAuth, setHeader } = useAuthHeader();
  const signIn = useSignIn();
  const [loginStatus, setLoginStatus] = useState(null);

  function validate() {
    return loginId.length > 0 && password.length > 0;
  }

  async function submit(event) {
  try{
    event.preventDefault();
    loginService.setLoginId(loginId);
    loginService.setPassword(password);
    const result = await loginService.validateLogin(loginId,password);
    if (result && result.response === "success") {
          signIn({
            token: result.token,
            expiresIn: 24*60*60,//24 hours
            tokenType: "Bearer",
            authState: { username: "admin" },
          });
          window.location.href = "/history";
    } else {
      setLoginStatus("invalid");
      console.log("Login failed...");
    }
    }catch(error){
        setLoginStatus("invalid");
        console.log("Error While login."+error);
    }
  }

  return (
    <form
      name="vishing_form"
      method="post"
      className="login_tab_form"
      onSubmit={submit}
    >
      <p align="center" style={{ fontWeight: 'bolder', fontSize: '18px', fontFamily: 'helvetica' }}>
        RedShift Login
      </p>
      <div>
        <label>Login ID</label>
        <input className="loginTextField"
          autoFocus
          type="text"
          value={loginId}
          onChange={(e) => setLoginId(e.target.value)}
        />
      </div>
      <div>
        <label>Password</label>
        <input className="loginTextField"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
          {loginStatus === "invalid" && (
            <p>
              Invalid credentials. Please try again.
            </p>
          )}
      <div>
        <button
          id="submit"
          type="submit"
          name="login"
          className="submitBtn"
          disabled={!validate()}
        >
          Login
        </button>
      </div>
      </div>
    </form>
  );
}

error:

Compiled with problems:
ERROR
[eslint] 
src\services\EndpointConfig.js
  Line 38:3:  React Hook "useSetupInterceptors" cannot be called at the top level. React Hooks must be called in a React function component or a custom React Hook function  react-hooks/rules-of-hooks

i am trying to learn react js please help how to fix this issue or any better way to handle the session management and browser closure handling

Thanks


Solution

  • You cannot call React hooks outside React components and custom React hooks. You can move the useSetupInterceptors hook call into your React code.

    Here's an example using a local component that is rendered within the AuthProvider so that it can call the useSetupInterceptors hook and the useSetupInterceptors hook can reference the useSignOut hook from `react-auth-kit';

    EndpointConfig.js

    import axios from 'axios';
    import useSetupInterceptors from '../util/useSetupInterceptors';
    
    const config = window.APP_CONFIG;
    const endPointURLIP = config.endPointIP;
    const defaultUrl = "localhost";
    
    if (!endPointURLIP) {
      endPointURLIP = defaultUrl;
    }
    
    const getTokenFromCookie = () => {
      const [, token = null] = document.cookie
        .split(";")
        .map(cookie => cookie.trim().split("="))
        .find(([key, value]) => key === "_auth") ?? [];
      return token;
    };
    
    const authToken = getTokenFromCookie();
    
    const getAxiosInstance = () => {
      const API_BASE_URL = 'http://' + endPointURLIP + ':9081';
      const AUTHENTICATION_HEADER_TOKEN = authToken ? `Bearer ${authToken}` : null;
    
      return axios.create({
        baseURL: API_BASE_URL,
        headers: {
          Authorization: AUTHENTICATION_HEADER_TOKEN,
        },
      });
    };
    
    export const authAxiosInstance = getAxiosInstance(); // <-- export for app usage
    
    const endpointConfig = {
      a: 'http://'+endPointURLIP+':9081',
      HISTORY_ENDPOINT: '/demo/history',
      AUTHENTICATION_HEADER_TOKEN: authToken ? `Bearer ${authToken}` : null,
      AUTHENTICATION_HEADER: authAxiosInstance,
    };
    
    export default endpointConfig;
    
    import React from 'react';
    import { BrowserRouter, Route, Routes } from 'react-router-dom';
    import { AuthProvider, AuthOutlet } from 'react-auth-kit';
    import DemoHistory from './components/DemoHistory';
    import useSetupInterceptors from '../util/useSetupInterceptors';
    import { authAxiosInstance } from './EndpointConfig';
    
    const SetupAxios = () => {
      useSetupInterceptors(authAxiosInstance);
    
      return null;
    };
    
    function App() {
      return (
        <AuthProvider
          authType={"cookie"}
          authName={"_auth"}
          cookieDomain={window.location.hostname}
          cookieSecure={false}
        >
          <BrowserRouter>
            <SetupAxios />
            <div>
              <h2 className="header_name">Demo</h2>
              <Routes>
                <Route element={<AuthOutlet fallbackPath='/' />}>
                  <Route path="DemoHistory" element={<DemoHistory />} />
                </Route>
                ...
              </Routes>
            </div>
          </BrowserRouter>
        </AuthProvider>
      );
    }
    
    export default App;