Fecha publicación: Nov 10, 2021

React Router 6 - Migrando interceptor

En React Router 5 una práctica común para gestionar el acceso publica o autenticado a una página, es crear un interceptor, es decir envolver el Router en un componente que haga el chequeo de acceso. La versión 6 de React Router ya no soporta esto, en este video vamos a ver como migrarlo.

Manos a la obra

Punto de partida

Partimos de un ejemplo de interceptor que funciona en React Router V5 (para ahorrar tiempo, en esta base de código ya hemos realizado parte de la migración a la versión 6).

Que piezas tenemos:

Por un lado tiramos de contexto para almacenar el usuario que se ha logado, esto lo podemos ver en el fichero /src/auth/authcontext.tsx

/src/auth/authcontext.tsx

import React from "react";

interface Context {
  userInfo: string;
  setUserInfo: (user: string) => void;
}

export const AuthContext = React.createContext<Context>({
  userInfo: "",
  setUserInfo: (user: string) =>
    console.log("Did you forgot to add AuthContext on top of your app?"),
});

export const AuthProvider: React.FunctionComponent = (props) => {
  const { children } = props;
  const [userInfo, setUserInfo] = React.useState<string>("");

  return (
    <AuthContext.Provider value={{ userInfo, setUserInfo }}>
      {children}
    </AuthContext.Provider>
  );
};

Este proveedor de contexto lo registramos a nivel de aplicación.

./src/app.tsx

export const App = () => {
  return (
    <AuthProvider>
      <Router>

El contexto lo usamos en el dialogo de login para almacenar la cuenta del usuario:

./src/pages/login.tsx

const { setUserInfo } = React.useContext(AuthContext);

const handleNavigation = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();

  if (username === "admin" && password === "test") {
    setUserInfo(username);
    navigate("/list");
  } else {
    alert("User / password not valid, psst... admin / test");
  }
};

Y en un interceptor

export const AuthRouteComponent: React.FunctionComponent<RouteProps> = (
  props
) => {
  const { userInfo } = React.useContext(AuthContext);
  const navigate = useNavigate();

  React.useEffect(() => {
    if (!userInfo) {
      navigate("/");
    }
  }, [props.location.pathname]);

  return <Route {...props} />;
};

Aquí comprobamos si el usuario está logado y si no redirigimos a la página de login.

Por otro lado fíjate que devolvemos un componente Route de react-router, esto nos permite definir las rutas de la siguiente manera:

export const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<LoginPage />} />
          <AuthRouteComponent path="/list" element={<ListPage />} />
          <AuthRouteComponent path="/detail/:id" element={<DetailPage />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
};

Lo interesante es que las ruta que son privadas (necesitan que el usuario este logado) las definimos usando el componente que hemos visto antes AuthRouteComponent, este wrapper se encarga de comprobar si el usuario está logado:

  • Si no lo está te redirige a la página de login.
  • Si lo está directamente te expone el route que tenía wrapeado.

Adaptando a la versión 6 de React Router

Vamos a ver que pasa con la versión 6 instalada:

La primera en la frente, no nos vale la propiedad location.pathname en vez de eso hay que usar:

./src/auth/authroute.tsx

  React.useEffect(() => {
    if (!userInfo) {
      navigate("/");
    }
-  }, [props.location.pathname]);
+  }, [props.path]);

Eeeey todo transpila no hay errores... ejecutamos y ¡ouch! página en blanco... miramos la consola y vemos el siguiente mensaje en rojo sangre:

Uncaught Error: [AuthRouteComponent] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>

Y ahora viene el breaking change que duele, para implementar nuestro interceptor usábamos un truco, react-router version 5 te permite poner cualquier tipo de componente debajo de switchRoutes esto ya no pasa en la versión 6, estamos forzados a usar Route, ¿Qué nos aconseja la documentación de React Router? How do I compose y que nos encontramos en "San StackOverflow", pues que hagamos el wrapper dentro de la propiedad element.

Esto implica cambiar el interceptor, en vez de devolver un router, lo que hago es pintar los children directamente:

  • Ya no recibimos las RouteProps ya que lo vamos a instanciar a nivel de componente.
  • Comprobamos la ruta cuando se monta el componente.
  • Devolvemos directamente el contenido wrapeado, usando la propiedad children.

./src/auth/authroute.tsx

- export const AuthWrapper: React.FunctionComponent<RouteProps> = (props) => {
+ export const AuthWrapper: React.FunctionComponent = (props) => {
  React.useEffect(() => {
    if (!userInfo) {
      navigate("/");
    }
-  }, [props.path]);
+  }, []);

-  return <Route {...props} />;
+  return <>{children}</>
};

Vamos a aprovechar cambiamos el nombre del componente y fichero para que tenga más sentido, lo llamaremos AuthWrapper.

./src/auth/authroute.tsx

- export const AuthRouteComponent: React.FunctionComponent<RouteProps> = (
+ export const AuthWrapper: React.FunctionComponent<RouteProps> = (

El fichero pasa a llamarse authwrapper.tsx

Para finalizar nuestro refactor, abrimos el fichero que contiene las rutas de la aplicación, y volvemos a usar rutas estándares en todos los casos y donde necesitamos el interceptor lo envolvemos dentro de la propiedad element de cada route

./src/app.tsx

import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { LoginPage, ListPage, DetailPage } from "./pages";
- import { AuthProvider, AuthRouteComponent } from "./auth";
+ import { AuthProvider, AuthRouteComponent } from "./auth";


export const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<LoginPage />} />
-          <AuthRouteComponent path="/list" element={<ListPage />} />
+          <Route path="/list" element={<AuthWrapper><ListPage /></AuthWrapper>} />
-          <AuthRouteComponent path="/detail/:id" element={<DetailPage />} />
+          <Route path="/detail/:id" element={<AuthWrapper><DetailPage /></AuthWrapper>} />
        </Routes>
      </Router>
    </AuthProvider>
  );
};

Ahora si, vamos a hacer un npm start y comprobamos que esto vuelve a funcionar.

¿Te apuntas a nuestro máster?

Si te ha gustado este ejemplo y tienes ganas de aprender Front End guiado por un grupo de profesionales ¿Por qué no te apuntas a nuestro Máster Front End Online Lemoncode? Tenemos tanto edición de convocatoria con clases en vivo, como edición continua con mentorización, para que puedas ir a tu ritmo y aprender mucho.