React: Invalid hook call, Creating a centralized JS file

React noob here seeking to understand the following error:

Invalid hook call. Hooks can only be called inside of the body of a function component.

I am trying to build an AuthService for my React app to centralize all my authorization related code (Login, Logout, Token Expiry, etc) into a AuthService.js file.

I have a relatively simple Login.js page/component that works great.

Login.js

import { useContext } from "react";
import { AppContext } from '../pages/app';
import api from '../../utils/api';

const Login = () => {

    // Submission of password form, performs userLogin
    function handleLogin(e) {
        e.preventDefault();
        api.userLogin(username, password, handleLoginResult);
    }

    // Callback for user login response
    function handleLoginResult(response, success) {
        if (!success) {
            alert(response);
        } else {
            const {setUserAuth} = useContext(AppContext);
            const auth_data = {"token": response.access_token};
            setUserAuth(auth_data);
            sessionStorage.setItem("auth", JSON.stringify(auth_data));
        }
    }

    // HTML page content
    return (
        <section id='login'>
            <div className='flex flex-col items-center'>
                <form onSubmit={handleLogin}>
                </form>
            </div>
        </section>
    )
}

export default Login;

However, I want to take the code from my handleLoginResult function (within the else statement) and move it into my new AuthService.js util.

Updated Login.js

import { AuthHandleLogin } from '../../utils/AuthService';
import api from '../../utils/api';

const Login = () => {

    // Submission of password form, performs userLogin
    function handleLogin(e) {
        e.preventDefault();
        api.userLogin(username, password, handleLoginResult);
    }

    // Callback for user login response
    function handleLoginResult(response, success) {
        if (!success) {
            alert(response);
        } else {
            AuthHandleLogin(response);
        }
    }

    // HTML page content
    return (
        <section id='login'>
            <div className='flex flex-col items-center'>
                <form onSubmit={handleLogin}>
                </form>
            </div>
        </section>
    )
}

export default Login;

AuthService.js

import { useContext } from "react";
import { AppContext } from '../pages/app';

const AuthHandleLogin = (response) => {
    const {setUserAuth} = useContext(AppContext);
    const auth_data = {"token": response.access_token};
    setUserAuth(auth_data);
    sessionStorage.setItem("auth", JSON.stringify(auth_data));
}

const AuthHandleLogout = () => {
    //Some future code here
}

export { AuthHandleLogin, AuthHandleLogout };

However when I try to run the updated code, I receive the fatal Invalid hook call error in the browser console.

I’ve done a lot of reading, but I still can’t figure out why it’s complaining about my useContext and setUserAuth state inside the AuthService. I understand it has something to do with the Hook being outside the original Login functional component.

I’m unclear how I can correct my AuthService.js file to resolve the error and accomplish the intended outcome?

>Solution :

You cant use useContext inside a callback like this since it breaks the rules of hooks. Hooks are not "normal" functions and can’t be called in certain places.

What you would do here is pass the value back up to the component, which can then set it. The useContext will move to the only place it can go — which is in the top level render function (the other place is within another hook which is in the top level render in and of itself, see further down).

const Login = () => {
     const {setUserAuth} = useContext(AppContext);

    // Submission of password form, performs userLogin
    function handleLogin(e) {
        e.preventDefault();
        api.userLogin(username, password, handleLoginResult);
    }

    // Callback for user login response
    function handleLoginResult(response, success) {
        if (!success) {
            alert(response);
        } else {
            AuthHandleLogin(response, (user) => setUserAuth(user));
        }
    }

    // HTML page content
    return (
        <section id='login'>
            <div className='flex flex-col items-center'>
                <form onSubmit={handleLogin}>
                </form>
            </div>
        </section>
    )
}

export default Login;
const AuthHandleLogin = (response, onSetUser) => {
    const auth_data = {"token": response.access_token};
    onSetUser(auth_data);
    sessionStorage.setItem("auth", JSON.stringify(auth_data));
}

If you really need the setting in some common function, you would instead create a reusable hook:

const useHandleLogin = () => {
    const {setUserAuth} = useContext(AppContext);
    return useCallback((response) => {
        const auth_data = {"token": response.access_token};
        setUserAuth(auth_data);
        sessionStorage.setItem("auth", JSON.stringify(auth_data));
    }, [])
}

And then

const Login = () => {
     const handleLogin = useHandleLogin();

    // Submission of password form, performs userLogin
    function handleLogin(e) {
        e.preventDefault();
        api.userLogin(username, password, handleLoginResult);
    }

    // Callback for user login response
    function handleLoginResult(response, success) {
        if (!success) {
            alert(response);
        } else {
            handleLogin(response)
        }
    }

    // HTML page content
    return (
        <section id='login'>
            <div className='flex flex-col items-center'>
                <form onSubmit={handleLogin}>
                </form>
            </div>
        </section>
    )
}

export default Login;

Leave a Reply