Cómo manejar rutas autenticadas con Next.js

Cómo crear un HOC (componente de orden superior) que puede redirigir condicionalmente a un usuario en función de su estado de inicio o cierre de sesión.

En Next.js, de forma predeterminada, todas sus rutas se tratan de la misma manera.

Si bien su aplicación específica puede incluir páginas o rutas que están destinadas solo para usuarios que han iniciado sesión, de fábrica, Next.js no proporcionar una forma de aislar estas páginas en función del estado de autenticación de un usuario.

Esto es de esperar ya que Next.js está diseñado para manejar un conjunto de tareas simple y bien definido. Mientras que puede utilizarse como front-end para una aplicación, como en el repetitivo Next.js de CheatCode, tradicionalmente se utiliza para generar sitios de marketing estáticos o sitios respaldados por un CMS sin encabezado.

Afortunadamente, resolver este problema no es demasiado complejo. Para solucionarlo, vamos a implementar dos componentes:

  1. authenticatedRoute que será una función que devuelve un componente de React envuelto con una verificación condicional del estado de autenticación del usuario y una redirección si un usuario no está disponible.
  2. publicRoute que será una función que devuelve un componente React envuelto con una verificación condicional del estado de autenticación del usuario y una redirección si hay un usuario presente.

Implementación de un componente de ruta autenticado

Primero, construyamos el esqueleto de nuestro HOC y discutamos cómo va a funcionar:

/components/AuthenticatedRoute/index.js

import React from "react";

const authenticatedRoute = (Component = null, options = {}) => {
 // We'll handle wrapping the component here.
};

export default authenticatedRoute;

Aquí, exportamos una función simple de JavaScript que toma dos argumentos:un React Component como primer argumento y un objeto de options como el segundo. El Component representa el componente de la página protegida que queremos renderizar condicionalmente.

Cuando vayamos a poner esto en uso, haremos algo como esto:

/pages//index.js

import authenticatedRoute from '../../components/AuthenticatedRoute';

const MyComponent = () => {
  [...]
};

export default authenticatedRoute(MyComponent, { pathAfterFailure: '/login' })

En el futuro, completemos nuestro HOC con el componente contenedor principal:

/components/AuthenticatedRoute/index.js

import React from "react";

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return AuthenticatedRoute;
};

export default authenticatedRoute;

Aquí, hemos llenado nuestro authenticatedRoute el cuerpo de la función con un componente React basado en clases. La idea aquí es que queremos utilizar el estado y, a continuación, el componentDidMount función para la clase para que podamos decidir si queremos representar el Component pasado , o redirigir al usuario fuera de él.

/components/AuthenticatedRoute/index.js


import React from "react";
import Router from "next/router";

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    componentDidMount() {
      if (this.props.isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/login");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return AuthenticatedRoute;
};

export default authenticatedRoute;

Ahora, con nuestro componentDidMount agregado, podemos ver nuestro comportamiento central implementado. En el interior, todo lo que queremos saber es "¿hay un usuario conectado o no?" Si hay hay un usuario registrado, queremos decir "adelante y renderiza el Component pasado ." Podemos ver que esto tiene lugar en el render() método del AuthenticatedRoute componente.

Aquí, decimos, siempre que loading es true , solo devuelve un <div /> vacío (o no mostrar nada al usuario). Si no cargando, simplemente ejecute el return declaración en la parte inferior del render() .

Lo que se consigue con esto es decir "hasta que sepamos que tenemos un usuario logueado, no mostrar nada, y si lo hacemos tiene un usuario conectado, muéstrele la página a la que intenta acceder".

De vuelta en componentDidMount() en el else declaración, estamos diciendo "está bien, no parece que el usuario haya iniciado sesión, así que vamos a redirigirlo". Para hacer la redirección en este ejemplo, estamos usando el enrutador integrado Next.js para hacer la redirección por nosotros, pero puede usar cualquier enrutador JavaScript o React que desee (por ejemplo, si estuviéramos usando React Router, haría this.props.history.push(options.pathAfterFailure || '/login') .

¿Tener sentido? Entonces, si tenemos un usuario, muéstrale el componente. Si no tenemos un usuario, redirigirlo a otra ruta.

Determinar el estado de inicio de sesión

Ahora, técnicamente hablando, esto es todo lo que tenemos que hacer. Pero es posible que se pregunte "¿cómo sabemos si el usuario ha iniciado sesión?" Aquí es donde entra en juego su propia aplicación. En este ejemplo, estamos usando CheatCode Next.js Boilerplate que depende de que un usuario autenticado (si está disponible) esté presente en una tienda Redux global.

Para hacer todo esto un poco más concreto, echemos un vistazo a esa configuración ahora:

/components/AuthenticatedRoute/index.js

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    componentDidMount() {
      if (this.props.isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/login");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return connect((state) => ({
    isLoggedIn: state?.authenticated && !!state?.user,
  }))(AuthenticatedRoute);
};

export default authenticatedRoute;

El gran cambio que hemos hecho aquí es importar el connect() método del react-redux paquete (ya instalado en el repetitivo) y luego llamar a esa función, pasándole un mapStateToProps función y luego envolviéndola alrededor de nuestro componente. Para ser claros, esta parte:

/components/AuthenticatedRoute/index.js

return connect((state) => ({
  isLoggedIn: state?.authenticated && !!state?.user,
}))(AuthenticatedRoute);

Aquí, la función que pasamos como primer argumento a connect() es el mapStateToProps función (como se nombra en el react-redux documentación). Esta función toma el estado global actual de la aplicación proporcionada por el <ReduxProvider /> en /pages/_app.js en el código modelo de CheatCode Next.js.

Usando ese estado, como su nombre lo indica, mapea ese estado a un accesorio de componente React que se entregará a nuestro <AuthenticatedRoute /> componente definido justo encima.

Si miramos de cerca, aquí estamos configurando una propiedad llamada isLoggedIn , verificando si el authenticated el valor en nuestro estado es true y si tenemos o no un user objeto en estado. si lo hacemos? ¡El usuario ha iniciado sesión! Si no, isLoggedIn es falso.

Si miras hacia atrás en el componentDidMount() función, aquí es donde estamos poniendo el nuevo isLoggedIn accesorio a usar.

Usar otras fuentes de autenticación

Si no usando CheatCode Next.js Boilerplate, la forma en que llega al estado autenticado de su usuario depende de su aplicación. Un ejemplo rápido y sucio de usar otra API se vería así:

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";
import { myAuthenticationAPI } from 'my-authentication-api';

const authenticatedRoute = (Component = null, options = {}) => {
  class AuthenticatedRoute extends React.Component {
    state = {
      loading: true,
    };

    async componentDidMount() {
      const isLoggedIn = await myAuthenticationAPI.isLoggedIn();

      if (isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/login");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return AuthenticatedRoute;
};

export default authenticatedRoute;

En este ejemplo, casi todo es idéntico, pero en lugar de anticipar un valor de autenticación proveniente de una tienda Redux, solo llamamos a nuestra API de autenticación (por ejemplo, Firebase) directamente, confiando en el valor de retorno de esa llamada como nuestro isLoggedIn estado.

Implementar un componente de ruta pública

Ahora, algunas buenas noticias:nuestro publicRoute el componente es idéntico a lo que vimos arriba con un pequeño cambio:

/components/PublicRoute/index.js

import React from "react";
import Router from "next/router";
import { connect } from "react-redux";

const publicRoute = (Component = null, options = {}) => {
  class PublicRoute extends React.Component {
    state = {
      loading: true,
    };

    componentDidMount() {
      if (!this.props.isLoggedIn) {
        this.setState({ loading: false });
      } else {
        Router.push(options.pathAfterFailure || "/documents");
      }
    }

    render() {
      const { loading } = this.state;

      if (loading) {
        return <div />;
      }

      return <Component {...this.props} />;
    }
  }

  return connect((state) => ({
    isLoggedIn: state?.authenticated && !!state?.user,
  }))(PublicRoute);
};

export default publicRoute;

¿Puedes distinguirlo? Arriba en el componentDidMount hemos agregado un ! para decir "si el usuario es no iniciado sesión, siga adelante y renderice el componente. Si están conectados, redirígelos".

Literalmente, la lógica inversa a nuestro authenticatedRoute . El punto aquí es que queremos usar el publicRoute() componente en rutas como /login o /signup para redirigir a los usuarios ya autenticados fuera de esas paginas. Esto garantiza que no tengamos problemas con la base de datos más adelante, como usuarios duplicados o sesiones de varios usuarios.

Terminando

En este tutorial, aprendimos un patrón simple para crear un HOC (componente de orden superior) para redirigir a los usuarios en nuestra aplicación en función de su estado de inicio de sesión (autenticación). Aprendimos cómo implementar el componente base que "envuelve" el componente que estamos tratando de proteger y cómo implementar la lógica central para manejar el proceso de representación y redirección.

También vimos ejemplos del uso de datos de autenticación reales para agregar algo de contexto y aclarar cómo este patrón puede funcionar en cualquier configuración de autenticación dentro de Next.js.


No