Aguarde chamadas de API antes de renderizar rota privada

Sep 05 2020

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

1 PrateekThapa Sep 05 2020 at 14:44

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>
  )
}
2 3limin4t0r Sep 05 2020 at 21:05

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.

1 deepakgupta191199 Sep 05 2020 at 21:38

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.