'Value of state within React context not updating when used in another component
I'm trying to use context in conjunction with state to do role-based routing with React router. Upon clicking sign in, the user data is fetched from the backend API and set as state within the context provider.
The issue is that when I then attempt to use that state in my authorization component that I wrap around my route, the state isn't updated (I presume). What am I missing here?
The context:
const CurrentUserContext = createContext({});
export const CurrentUserProvider = ({children}) => {
const [currentUser, setCurrentUser] = useState(null)
const [currentUserRole, setCurrentUserRole] = useState(null)
const requestOption = {
method: 'GET',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'Authorization': `Token ${localStorage.getItem('token')}`
}
}
const fetchCurrentUser = () => {
fetch('http://127.0.0.1:8000/api/users-current/', requestOption)
.then(response => response.json())
.then(data => {
console.log('called')
setCurrentUser(data['user'])
setCurrentUserRole(data['role'])
})
.catch(error => console.log(error))
}
return (
<CurrentUserContext.Provider value={{
setCurrentUser,
setCurrentUserRole,
currentUser,
currentUserRole,
fetchCurrentUser
}}>
{children}
</CurrentUserContext.Provider>
)
}
export const useAuth = () => useContext(CurrentUserContext)
The sign in component where I run fetchCurrentUser on sign in to set the state etc:
export default function SignIn() {
const { fetchCurrentUser } = useAuth()
const handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const credentials = btoa(`${data.get('username')}:${data.get('password')}`);
const requestOptions = {
method: "POST",
credentials: 'include',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
"Authorization": `Basic ${credentials}`
},
body: JSON.stringify({})
}
fetch('http://127.0.0.1:8000/api/login/', requestOptions)
.then(response => response.json())
.then(data => {
localStorage.setItem('token', data['token'])
localStorage.setItem('role', data['role'])
})
.then(fetchCurrentUser())
.catch(error => console.log(error))
}
The component where I'm trying to access the currentUser state:
import { useLocation, Navigate, Outlet } from "react-router";
import { useAuth } from "./CurrentUserContext"
const RequireAuth = () => {
const { currentUser } = useAuth()
const location = useLocation()
return (
currentUser
? <Outlet />
: <Navigate
to="/"
state={{ from: location }}
replace
/>
)
}
export default RequireAuth;
And finally my routes:
export default function App() {
return (
<CurrentUserProvider>
<DialogProvider>
<Router>
<Routes>
<Route element={<RequireAuth/>}>
<Route path="roles" element={<DashboardContent role={true}/>}/>
</Route>
<Route path="/" element={<SignIn/>}/>
</Routes>
</Router>
</DialogProvider>
</CurrentUserProvider>
If I navigate to roles after signing in I'm just redirected to the sign in page again since currentUser is null.
Solution 1:[1]
I think this may be a stale enclosure over the requestOption object in the CurrentUser component. The SignIn component only updates the localStorage in the handleSubmit handler before calling fetchCurrentUser which uses the closed over requestOption object that hasn't pulled in the updated token value from localStorage.
Move the options into the fetchCurrentUser callback so they are computed on-demand from the current localStorage value.
export const CurrentUserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [currentUserRole, setCurrentUserRole] = useState(null);
const fetchCurrentUser = () => {
const requestOption = {
method: 'GET',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
'Authorization': `Token ${localStorage.getItem('token')}`
}
};
fetch('http://127.0.0.1:8000/api/users-current/', requestOption)
.then(response => response.json())
.then(data => {
console.log('called');
setCurrentUser(data.user);
setCurrentUserRole(data.role);
})
.catch(console.log);
};
return (
<CurrentUserContext.Provider
value={{
setCurrentUser,
setCurrentUserRole,
currentUser,
currentUserRole,
fetchCurrentUser
}}
>
{children}
</CurrentUserContext.Provider>
);
};
...
export default function SignIn() {
const { fetchCurrentUser } = useAuth();
const handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const credentials = btoa(`${data.get('username')}:${data.get('password')}`);
const requestOptions = {
method: "POST",
credentials: 'include',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
"Authorization": `Basic ${credentials}`
},
body: JSON.stringify({})
};
fetch('http://127.0.0.1:8000/api/login/', requestOptions)
.then(response => response.json())
.then(data => {
localStorage.setItem('token', data['token'])
localStorage.setItem('role', data['role'])
})
.then(fetchCurrentUser) // <-- don't immediately invoke!
.catch(error => console.log(error));
};
...
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
