Aguarde chamadas de API antes de renderizar rota privada
Eu tenho rotas protegidas que passam auth habilitado true ou false para permitir que os usuários acessem a página de autenticação. Cada carregamento de página, ele chama a API de token de autenticação e verifica se é válido. Se o token for inválido, redirecione para a página de login.
ProtectedRoute.js
const ProtectedRoute = ({isEnabled, ...props}) => {
return (isEnabled) ? <Route {...props} /> : <Redirect to="/login"/>;
};
export default ProtectedRoute;
Routes.js
import {withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [isAuth, setIsAuth] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
})()
},[isAuth])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} isEnabled={isAuth} />
<Route path="/" component={Login} />
</Switch>
)
}
App.js
const history = createBrowserHistory();
function App() {
return (
<Router history={history}>
<Routes/>
</Router>
)
}
export default App;
Dashboard.js
export const Dashboard = () => {
return (
<div class="ui form-user center raised padded segment">
<a href="/section1">
<div id="section1" class="ui floated right basic red button">
Directory #1
</div>
</a>
</div>
)
}
Problema
Quando os usuários de autenticação navegam na página autenticada (Painel), o redireciona para a página de login. O motivo é que a API de token de verificação retorna após ProtectedRoute renderizado como isAuth igual a false.
Respostas
Você pode usar o estado inicial nullpara diferenciar os diferentes estados de seu componente.
- null -> token de API ainda não chamado.
- verdadeiro -> token verificado
- false -> verificação de token falhou.
export default function ProtectedRoute(props) {
const [isAuth, setIsAuth] = React.useState(false)
const history = useHistory();
React.useEffect(() => {
async function validateToken() {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(true)
return;
}
}
history.push('/login')
}
validateToken();
}, [isAuth]);
if (isAuth === null) return null;
return props.children;
}
export default function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={HomePage} />
<Route exact path="/login" component={Login} />
<ProtectedRoute>
<Route path="/dashboard" component={Dashboard} />
</ProtectedRoute>
</Switch>
</Router>
)
}
Em vez de isAuthser um booleano, você usa uma string ou número para aumentar o número de estados. Por exemplo, renomeie isAuthpara loginStateque pode ser "pending", "authenticated"ou "unauthenticated". Em seguida, use "pending"como o estado inicial e adicione um cenário adicional. Você poderia, por exemplo, retornar nullpara não renderizar nada, renderizar um círculo giratório, etc.
Aqui está um exemplo de renderização null(nada) enquanto o token de login está sendo autenticado:
ProtectedRoute.js
const loginRoutes = {
pending: ( ) => null,
authenticated: (...props) => <Route {...props} />,
unauthenticated: ( ) => <Redirect to="/login"/>,
};
const ProtectedRoute = ({loginState = "pending", ...props}) => {
const LoginRoute = loginRoutes[loginState];
return <LoginRoute {...props} />;
};
export default ProtectedRoute;
Route.js
import { withRouter, Switch, Route } from "react-router-dom";
export default withRouter(({ location }) => {
const [loginState, setLoginState] = useState("pending")
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200) {
setLoginState(res.data.valid ? "authenticate" : "unauthenticated")
} else setLoginState("unauthenticated");
} else setLoginState("unauthenticated")
})()
}, [loginState])
return (
<Switch>
<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />
<Route path="/" component={Login} />
</Switch>
)
}
Como isenção de responsabilidade, não tenho experiência com o React Router, então mantive o exemplo o mais próximo possível do original.
Você pode usar outra variável de estado para aguardar a execução da API antes de inicializar as rotas.
const [isAuth, setIsAuth] = useState(false)
const [checked, setChecked] = useState(false)
useLayoutEffect(() => {
(async() => {
if(accessToken){
let res = await ValidateLoginToken(accessToken)
if (res && res.data && res.data.status === 200){
setIsAuth(res.data.valid)
} else setIsAuth(false);
} else setIsAuth(false)
setChecked(true)
})()
},[isAuth])
Em seguida, no Routes, você pode fazer algo como:
<Switch>
{
!checked?(<React.Fragment/>):!isAuth?(<Route path="/" component={Login} />):(<ProtectedRoute path="/dashboard" component={Dashboard} loginState={loginState} />)
}
</Switch>
Eu geralmente separo as rotas de sessão e nenhuma rota de sessão em ganchos separados e funciona bem.