I am using
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.
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
}
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])
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.
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 />
);
}
};
import { roleCheck } from '../Utils/RoleCheck';
...
<Button variant="outlined" disabled={roleCheck("organisation", 2)} color="primary">
edit organisation
</Button>
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
}
}
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With