'React component rendering too many times?
I've got a react app using ReduxToolkit and React.useState in the App component.
It's written to attempt a token refresh on the first render. Then, based on the auth state after refresh, either render the desired route or render the signin route (not using redirects, just state and conditional rendering)
My expectation is that upon heading to the homepage ('/'), useEffect triggers, attempts token refresh, dispatches the token/user credentials, setLoading(false), then renders the homepage ('/') if successful, or the login page if not successful.
The problem is, it seems to be rendering the component a few times while it's performing the token refresh, before it's even received the refresh result, causing it to flicker to the login page, then to the main dashboard, when it should just be a loadscreen, then the main dashboard.
I feel like the setLoading(false) paired with the redux dispatch could be causing multiple re-renders but I don't understand why the re-rendering would happen BEFORE the refresh request has happened.
StrictMode is not being used, I understand it renders the page twice in order to show data on problems it detects.
The loading state defaults to true, so first render should always be a loadscreen/fullscreen spinner.
The console contents when I refresh the page at '/', where the routes are logged from its respective RouteProtector component (Protect.tsx)
UseEffect refresh App.tsx:47
render App.tsx:20
attempting refresh of access token App.tsx:22
refresh token in localStorage App.tsx:24
/ Protect.tsx:15
/signin Protect.tsx:15
refresh success App.tsx:39
/signin Protect.tsx:15
/ Protect.tsx:15
App.tsx:
import React, { useEffect, useState } from "react";
import { Route, Routes } from "react-router-dom";
import SignUpView from "./views/auth/SignUpView";
import SignInView from "./views/auth/SignInView";
import Protect from "./components/auth/Protect";
import { useAppDispatch } from "./hooks/store";
import { Spinner } from "react-bootstrap";
import DashboardView from "./views/DashboardView";
import UserListView from "./views/users/UserListView";
import EditUserView from "./views/users/EditUserView";
import UserDetailView from "./views/users/UserDetailView";
import { setCredentials } from "./features/auth/authSlice";
import { RefreshResponse } from "./models/Auth";
function App() {
const dispatch = useAppDispatch();
const [loading, setLoading] = useState(true);
const refresh = async () => {
console.log("render")
const token = localStorage.getItem("refreshToken");
console.log("attempting refresh of access token")
if (token) {
console.log("refresh token in localStorage")
const refreshRequest = {
refresh: token,
};
const response = await fetch(
"http://my.api.com/auth/refresh/",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
}
);
if (response.status === 200) {
console.log("refresh success");
const data: RefreshResponse = await response.json();
dispatch(setCredentials({ user: data.user, token: data.accessToken }));
}
}
};
useEffect(() => {
console.log("UseEffect refresh")
refresh();
setLoading(false)
}, []);
if (loading) return (
<div className="h-100 d-flex justify-content-center align-items-center bg-dark">
<Spinner animation="border" />
</div>
);
// the route prop on the Protect components below is just for debugging, it gets printed when rendered
return (
<>
<Routes>
<Route
path="/"
element={
<Protect route="/">
<DashboardView />
</Protect>
}
/>
<Route
path="/signup"
element={
<Protect route="/signup" inverse>
<SignUpView />
</Protect>
}
/>
<Route
path="/signin"
element={
<Protect route="/signin" inverse>
<SignInView />
</Protect>
}
/>
<Route
path="/users"
element={
<Protect route="/users">
<UserListView />
</Protect>
}
/>
<Route
path="/users/:id"
element={
<Protect route="/users:id">
<UserDetailView />
</Protect>
}
/>
<Route
path="/users/:id/edit"
element={
<Protect route="/users/:id/edit">
<EditUserView />
</Protect>
}
/>
</Routes>
);
</>
);
}
export default App;
Protect.tsx:
import React from "react";
import { Navigate } from "react-router-dom";
import { User } from "../../models/User";
import { useTypedSelector } from "../../hooks/store";
interface IProtectProps {
inverse?: boolean;
staff?: boolean;
children?: React.ReactElement;
route?: string
}
const Protect: React.FC<IProtectProps> = ({ children, inverse, staff, route }) => {
const user = useTypedSelector((state) => state.auth.user) as User;
console.log(route);
if (staff)
return <>{user && user.is_staff ? children : <Navigate to="/" />}</>;
if (!inverse) return <>{user ? children : <Navigate to="/signin" />}</>;
return <>{user ? <Navigate to="/" /> : children}</>;
};
export default Protect;
Solution 1:[1]
I managed to solve the problem by moving the setLoading(false) line to the end of the refresh() function. I'm still a little confused as this is how I originally had it before moving it after the refresh() function inside the useEffect() call.
I also changed the useEffect dependency to dispatch. The app now only renders twice, once for the loadscreen while it refreshes the token, and then once for the actual page render that's desired.
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log("UseEffect refresh")
const refresh = async () => {
console.log("render");
const token = localStorage.getItem("refreshToken");
console.log("attempting refresh of access token");
if (token) {
console.log("refresh token in localStorage");
const refreshRequest = {
refresh: token,
};
const response = await fetch(
"http://jrapi.subspacedev.io/auth/refresh/",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(refreshRequest),
}
);
if (response.status === 200) {
console.log("refresh success");
const data: RefreshResponse = await response.json();
dispatch(
setCredentials({ user: data.user, token: data.accessToken })
);
}
}
setLoading(false);
};
refresh();
}, [dispatch]);
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 | Jordan Renaud |
