Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React has detected a change in the order of Hooks called, but I can't see any hooks being called conditonally

I am using

  • ReactJS 16.14.0

I have a React functional component that relies on data stored in context to render correctly, some of this data needs additional processing before display and some additional data needs fetching. the component is throwing the React has detected a change in the order of Hooks error, I have read the react docs on the rules of hooks as well as having a good look through SO but I can't work out why I get the error. I have shortened the code below to keep it brief.


    const { enqueueSnackbar } = useSnackbar();
    const [ mainContact, setMainContact ] = useState(undefined);
    const [ mainAddress, setMainAddress ] = useState(undefined);
    const [ thisLoading, setThisLoading ] = useState(true);
    const { organisation, addresses, loading } = useProfileState();


    useEffect(() => {
        setThisLoading(true);
        if(organisation && addresses && !loading) {
            Promise.all([getMainContact(), getMainAddress()])
            .then(() => {
                setThisLoading(false);
            })
            .catch(() => {
                console.log("Failed getting address/contact info");
                setThisLoading(false);
            })
        }
    }, [organisation, addresses, loading])


    const getMainContact = () => {
        return new Promise((resolve, reject) => {
            apiService.getData(`/organisation/users/${organisation.mainContact}`)
            .then(mainContact => {
                setMainContact(mainContact);
                return resolve();
            })
            .catch(error => {
                enqueueSnackbar(error, { variant: 'error' });
                return reject();
            })
        })
    }

    const getMainAddress = () => {
        return new Promise((resolve, reject) => {
            let mainAddress = addresses.find(addr => addr.id === organisation.mainAddress)
            if(mainAddress !== undefined) {
                setMainAddress(mainAddress);
                return resolve();
            } else {
                enqueueSnackbar("Error getting main address ", { variant: 'error' });
                return reject();
            }
        })
    }
}

I just want to understand why I get this error and any potential solutions or what I am doing wrong etc. below is the full error. If I comment out the setThisLoading(false) in the .then() of the Promise.all() the error goes away but my page never displays any content because I use thisLoading to conditionally render a loading wheel or the content.

   ------------------------------------------------------
1. useContext                 useContext
2. useDebugValue              useDebugValue
3. useContext                 useContext
4. useRef                     useRef
5. useRef                     useRef
6. useRef                     useRef
7. useMemo                    useMemo
8. useEffect                  useEffect
9. useEffect                  useEffect
10. useDebugValue             useDebugValue
11. useContext                useContext
12. useState                  useState
13. useState                  useState
14. useState                  useState
15. useState                  useState
16. useState                  useState
17. useState                  useState
18. useState                  useState
19. useContext                useContext
20. useEffect                 useEffect
21. undefined                 useContext
   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I am just looking to understand why the setThisLoading(false) causes me to get this error.

Update

The useSnackbar() hook is provided by an external libary notistack https://github.com/iamhosseindhv/notistack

Below is the code relating to useProfileState()

import React, { createContext, useContext, useReducer } from 'react';

const initialState = { loggedIn: false, loading: true, error: false }

const ProfileStateContext = createContext();
const ProfileDispatchContext = createContext();

const ProfileReducer = (state, action) => {
    switch (action.type) {
        case 'LOGGED_IN':
            console.log("PROFILE CONTEXT - Logged in");
            return { ...state, loggedIn: true }
        case 'LOADED':
            console.log("PROFILE CONTEXT - Data loaded");
            return { ...state, loading: false, error: false }
        case 'LOADING':
            console.log("PROFILE CONTEXT - Data loading");
            return { ...state, loading: true, error: false }
        case 'ERROR':
            console.log("PROFILE CONTEXT - Error");
            return { ...state, loading: false, error: true }
        case 'ADD_USER':
            console.log("PROFILE CONTEXT - Adding user...");
            return { ...state, user: { ...action.payload } }
        case 'ADD_ORGANISATION':
            console.log("PROFILE CONTEXT - Adding organisation...");
            return { ...state, organisation: { ...action.payload } }
        case 'ADD_ROLES':
            console.log("PROFILE CONTEXT - Adding roles...");
            return { ...state, roles: [...action.payload] }
        case 'ADD_ORGANISATIONS':
            console.log("PROFILE CONTEXT - Adding organisations...");
            return { ...state, organisations: [...action.payload] }
        case 'ADD_ADDRESSES':
            console.log("PROFILE CONTEXT - Adding addresses...");
            return { ...state, addresses: [...action.payload] }
        case 'LOGOUT':
            console.log("PROFILE CONTEXT - Removing context data...");
            return initialState;
        default:
            console.error(`Unhandled action dispatched to user reducer, action type was: ${action.type}`);
            return state;
    }
}

const ProfileProvider = ({ children }) => {

    const [state, dispatch] = useReducer(ProfileReducer, initialState)

    return (
        <ProfileStateContext.Provider value={state}>
            <ProfileDispatchContext.Provider value={dispatch}>
                {children}
            </ProfileDispatchContext.Provider>
        </ProfileStateContext.Provider>
    );
};

const useProfileState = () => {
    const context = useContext(ProfileStateContext);

    if (context === undefined) {
        throw new Error('useProfileState must be used within a ProfileContextProvider')
    }

    return context;
};

const useProfileDispatch = () => {
    const context = useContext(ProfileDispatchContext);

    if (context === undefined) {
        throw new Error('useProfileDispatch must be used within a ProfileContextProvider')
    }

    return context;
};

export {
    ProfileProvider,
    useProfileDispatch,
    useProfileState
}

Update 2

I have also tried chaining the promises and adding a dummy cleanup func as suggested, I still get the same error.

useEffect(() => {
    setThisLoading(true);
    if(organisation && addresses && !loading) {
        getMainContact()
            .then(() => {
                getMainAddress()
                    .then(() => {
                        getBillingContact()
                            .then(() => {
                                getBillingAddress()
                                    .then(() => {
                                        setThisLoading(false);
                                    })
                            })
                    })
            })
    }

    return () => {};
}, [organisation, addresses, loading])
like image 581
BlindCoding Avatar asked Oct 19 '25 10:10

BlindCoding


1 Answers

I found the problem to be in a completely different component to the one the error was indicating towards. The setThisLoading(false) was a red herring as this just allowed the problem component to render therefore giving the error. The way I found this out was via Chrome’s console, I usually work in Firefox as this is my browser of choice but this time Chrome came to the rescue as it gave more information as to where the error was originating from.

The application I am building has the concept of user roles, allowing/denying users to perform certain tasks. I wrote some functions to assist in the disabling of buttons and/or not showing content based on the role the logged in user had. This is where the problem lies.

Old RoleCheck.js

    import React from 'react';
    import { useProfileState } from '../../context/ProfileContext';

    //0 - No permission
    //1 - Read only
    //2 - Read/Write

    const _roleCheck = (realm, permission) => {

        const { organisation, organisations, roles, loading } = useProfileState();

        if(!loading) {

            //Get the RoleID for the current account
            const currentOrganisation = organisations.find(org => org.id === organisation.id);
            //Get the Role object by RoleID
            const currentRole = roles.find(role => role.id === currentOrganisation.roleId);

            if(currentRole[realm] === permission) {
                return true;
            } else {
                return false;
            }
        }

    };

    export const roleCheck = (realm, permission) => {

        //Reversed boolean for button disabling
        if(_roleCheck(realm, permission)) {
            return false;
        } else {
            return true;
        }

    };

    export const RoleCheck = ({ children, realm, permission }) => {
        
        if(_roleCheck(realm, permission)) {
            return (
                <React.Fragment>
                    { children }
                </React.Fragment>
            );
        } else {
            return (
                <React.Fragment />
            );
        }

    };

Usage of old RoleCheck.js

import { roleCheck } from '../Utils/RoleCheck';
...
<Button variant="outlined" disabled={roleCheck("organisation", 2)} color="primary">
    edit organisation
</Button>

New RoleCheck.js

import React from 'react';
import { useProfileState } from '../../context/ProfileContext';

//0 - No permission
//1 - Read only
//2 - Read/Write

export const useRoleCheck = () => {

    const { organisation, organisations, roles, loading } = useProfileState();

    const _roleCheck = (realm, permission) => {
    
        if(!loading) {
    
            //Get the RoleID for the current account
            const currentOrganisation = organisations.find(org => org.id === organisation.id);
            //Get the Role object by RoleID
            const currentRole = roles.find(role => role.id === currentOrganisation.roleId);
    
            if(currentRole[realm] === permission) {
                return true;
            } else {
                return false;
            }
        }
    
    };

    const RoleCheckWrapper = ({ children, realm, permission }) => {
    
        if(_roleCheck(realm, permission)) {
            return (
                <React.Fragment>
                    { children }
                </React.Fragment>
            );
        } else {
            return (
                <React.Fragment />
            );
        }
    
    };

    const roleCheck = (realm, permission) => {

        //Reversed boolean for button disabling
        if(_roleCheck(realm, permission)) {
            return false;
        } else {
            return true;
        }
    
    };

    return {
        roleCheck: roleCheck,
        RoleCheckWrapper: RoleCheckWrapper
    }

}

Usage of new RoleCheck.js

import { useRoleCheck } from '../Utils/RoleCheck';
...
const RequiresRoleCheck = () => {

    const rc = useRoleCheck();

    return (
        <Button variant="outlined" disabled={rc.roleCheck("organisation", 2)} color="primary">
            edit organisation
        </Button>
    )

}

By turning my Role Check functions into a hook I am able to call hooks inside it and I am able to call the useRoleCheck() hook at the top level of components that need to use it therefore not breaking the rules of hooks!

like image 71
BlindCoding Avatar answered Oct 21 '25 23:10

BlindCoding