React, parte 7: Contexto

En esta ocasión vamos a ver cómo crear un contexto, una envoltura o súper componente que contendrá dentro suyo varios elementos que compartirán información y funcionalidades

Supongamos que agregamos a nuestras páginas dos componentes nuevos:

Login.jsx:

import Col from 'react-bootstrap/Col';
import Form from 'react-bootstrap/Form';
import Row from 'react-bootstrap/Row';
import Button from 'react-bootstrap/Button';

export const Login = () => {

    return (
        <Form>
            <Form.Group as={Row} className="mb-3" controlId="formPlaintextEmail">
                <Form.Label column sm="2">
                    Email
                </Form.Label>
                <Col sm="10">
                    <Form.Control type="email" placeholder="Ingrese su correo electrónico" />
                </Col>
            </Form.Group>
            <Form.Group as={Row} className="mb-3" controlId="formPlaintextPassword">
                <Form.Label column sm="2">
                    Password
                </Form.Label>
                <Col sm="10">
                    <Form.Control type="password" placeholder="Password" />
                </Col>
            </Form.Group>
            <Button variant="primary"> Login </Button>
        </Form>
    );
}

Y Welcome.jsx:

export const Welcome = () => {
    return (
        <>
            <h2> Bienvenida/o </h2>
        </>
    )
}

El primero será un simple formulario de login (El logueo lo vamos a simular, no va a ser real)

Y el segundo es la página de bienvenida que se le da a los usuarios logueados.

Vamos a importar las páginas nuevas en main.jsx:

import { Login } from './components/Login';
import { Welcome } from './components/Welcome';

Agregarlas a la lista de URL:

<Route path='/login' element={<Login />} />
<Route path='/welcome' element={<Welcome />} />

Por último actualizar los enlaces en Menu.jsx:

<Link className="nav-link" to="/login"> Login </Link>
<Link className="nav-link" to="/welcome"> Welcome </Link>
<a href="#" className="nav-link"> Logout </a>

A nivel visualización tenemos un problema.

El menú login sólo debería visualizarse a aquellos usuarios que no hayan iniciado sesión.

Mientras que el menú de welcome y logout, sólo debería ser visible para los personas que sí se autenticaron. El primer enlace será una página principal para los usuarios logueados, mientras que el segundo una acción para cerrar sesión.

Componentes padres, hijos, nietos y hasta hermanos

Hasta ahora nosotros vimos cómo pasar información desde un componente padre a un hijo, mediante los props. Sin embargo, en una aplicación es muy probable que la información se trasmita sin importar una jerarquía definida.

Por ejemplo, si el usuario se loguea en el componente Login.jsx, inmediatamente debería actualizarse el componente Menu.jsx mostrando las nuevas opciones.

Creando un contexto

Vamos a crear una directorio nuevo (esto no es obligatorio) llamado «context» donde agregaremos nuestro componentes envoltorios:

Dentro vamos a crear un archivo con el siguiente nombre: AuthContext.jsx

Lo editamos con el siguiente código:

import { createContext, useContext, useState } from "react";

const AuthContext = createContext();

export const AuthProvider = ( {children} ) => {

    const [is_logueado, setIsLogueado] = useState(false);

    const getIsLogueado = () => {
        return is_logueado;
    };

    const logIn = () => {
        setIsLogueado(true);
    };

    const logOut = () => {
        setIsLogueado(false);
    };

    return (
        <AuthContext.Provider value={ {getIsLogueado, logIn, logOut} }>
            {children}
        </AuthContext.Provider>
    );

}

export const useAuth = () => {
    return useContext(AuthContext);
}

Analizamos un segundo el código:

Primero creamos un contexto:

const AuthContext = createContext();

También vamos a tener un estado para guardar si el usuario está logueado o no:

const [is_logueado, setIsLogueado] = useState(false);

Habrá tres funciones:

const getIsLogueado = () => {
    return is_logueado;
};

const logIn = () => {
    setIsLogueado(true);
};

const logOut = () => {
    setIsLogueado(false);
};

Que se compartirán en el provider:

return (
    <AuthContext.Provider value={ {getIsLogueado, logIn, logOut} }>
        {children}
    </AuthContext.Provider>
);

El provider es un componente que envolerá al resto de los componentes hijos (nietos, etc) en donde nosotros queremos compartir funcionalidades. {children} justamente hace referencia esto último.

Y por último vamos a usar el hook useContext, el cual simplifica la información que se compartirá en los componentes hijos:

export const useAuth = () => {
    return useContext(AuthContext);
}

En pocas palabras, cada vez que desde un componente hijo queramos acceder a algunas de las funciones del contexto, vamos a llamar useAuth, que nos devolverá un objeto con las funciones (getIsLogueado, logIn y logOut)

Provider

Ahora, para que el resto de los componentes pueda actualizarse con el componente superior, debemos envolver nuestras rutas dentro del súper componente AuthProvider. En main.js hacemos lo siguiente:

<AuthProvider>
    <Router>
        <Menu />
        <div className='container mt-3'>
          <Routes>
            <Route path='/' element={<Inicio />} />
            <Route path='/info' element={<Info />} />
            <Route path='/posts' element={<PostsList />} />
            <Route path='/posts/:idPost' element={<PostShow />} />
            <Route path='/login' element={<Login />} />
            <Route path='/welcome' element={<Welcome />} />
            <Route path='*' element={<Error404 />} />
          </Routes>
        </div>
    </Router>
</AuthProvider>

(No olvidarse de: import { AuthProvider } from ‘./context/AuthContext’ )

useContext

Sólo nos resta acceder a las funcionalidades del contexto mediante los componentes hijos.

Vamos a empezar por Menu.jsx:

Importamos useAuth desde: AuthContext.jsx, que llamaba al hook useContext, y nos devuelve las funciones del contexto:

import { useAuth } from '../context/AuthContext';

Recuperamos mediante desestructuración las funciones getIsLogueado y logOut:

const { getIsLogueado, logOut } = useAuth();

Y reemplazamos en Menu.jsx:

<Link className="nav-link" to="/login"> Login </Link>
<Link className="nav-link" to="/welcome"> Welcome </Link>
<a href="#" className="nav-link"> Logout </a>

Por:

{   
    (getIsLogueado())
    ?
        <>
            <Link className="nav-link" to="/welcome"> Welcome </Link>
            <a href="#" className="nav-link"> Logout </a>
        </>
    :
        <Link className="nav-link" to="/login"> Login </Link>                            
}

Explicando un poco el código, lo que vamos a hacer es condicionar la visualización del componente. Si getIsLogueado nos devuelve un valor true (el usuario inició sesión) vamos a ver los enlaces de Welcome y para hacer logout. Si en cambio nos devuelve false (el usuario NO inició sesión) entonces será el enlace de login.

También vamos a agregar la funcionalidad al botón de login, que al hacer click sobre la misma llame a la función logOut y redireccione al componente Login.

Agregamos a:

import { Link } from 'react-router-dom';

La función de useNavigate:

import { Link, useNavigate } from 'react-router-dom';

Esa función, aunque no la vimos en la publicación pasada, nos permitirá redireccionar desde una ruta a otra.

Agregamos al componente Menu.jsx:

const navigate = useNavigate();

const logOutMenu = () => {
    //Cambiamos el estado del contexto de logueado a NO logueado
    logOut();
    //Redireccionamos a "/login"
    navigate("/login");
}

Y a la vista la función que creamos logOutMenu:

<a href="#" className="nav-link" onClick={logOutMenu}> Logout </a>

El código finalmente quedará:

import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

export const Menu = () => {

    const { getIsLogueado, logOut } = useAuth();

    const navigate = useNavigate();

    const logOutMenu = () => {
        //Cambiamos el estado del contexto de logueado a NO logueado
        logOut();
        //Redireccionamos a "/login"
        navigate("/login");
    }

    return (
        <Navbar expand="lg" className="bg-body-tertiary">
            <Container>
                <Navbar.Brand href="#home">React-Bootstrap</Navbar.Brand>
                <Navbar.Toggle aria-controls="basic-navbar-nav" />
                <Navbar.Collapse id="basic-navbar-nav">
                    <Nav className="me-auto">
                        <Link className="nav-link" to="/"> Inicio </Link>
                        <Link className="nav-link" to="/info"> Info </Link>
                        <Link className="nav-link" to="/posts"> Blog </Link>                    
                        {   
                            (getIsLogueado())
                            ?
                                <>
                                    <Link className="nav-link" to="/welcome"> Welcome </Link>
                                    <a href="#" className="nav-link" onClick={logOutMenu}> Logout </a>
                                </>
                            :
                                <Link className="nav-link" to="/login"> Login </Link>                            
                        }
                    </Nav>
                </Navbar.Collapse>
            </Container>
        </Navbar>
    );
}

Por último vamos a editar el componente de Login.jsx:

import Col from 'react-bootstrap/Col';
import Form from 'react-bootstrap/Form';
import Row from 'react-bootstrap/Row';
import Button from 'react-bootstrap/Button';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';

export const Login = () => {

    const { logIn } = useAuth();
    const navigate = useNavigate();

    const logInForm = () => {
        //Cambiamos el estado del contexto de NO logueado a logueado
        logIn();
        //Redireccionamos a "/welcome"
        navigate("/welcome");
    };

    return (
        <Form>
            <Form.Group as={Row} className="mb-3" controlId="formPlaintextEmail">
                <Form.Label column sm="2">
                    Email
                </Form.Label>
                <Col sm="10">
                    <Form.Control type="email" placeholder="Ingrese su correo electrónico" />
                </Col>
            </Form.Group>
            <Form.Group as={Row} className="mb-3" controlId="formPlaintextPassword">
                <Form.Label column sm="2">
                    Password
                </Form.Label>
                <Col sm="10">
                    <Form.Control type="password" placeholder="Password" />
                </Col>
            </Form.Group>
            <Button variant="primary" onClick={logInForm}> Login </Button>
        </Form>
    );
}

(Estamos simulando el login)

Rutas privadas

Si bien el enlace de welcome no se ve si el usuario no está logueado, éste no está protegido, es decir que cualquier persona podrá acceder con sólo ingresar «/welcome»

Deberíamos indicar que welcome es una ruta privada.

Para eso vamos a crear un nuevo componente (dentro de la carpeta context) con el nombre: ProtectedRoute.jsx

import { Navigate } from "react-router-dom";
import { useAuth } from "./AuthContext"

export const ProtectedRoute = ( {children} ) => {

    const {getIsLogueado} = useAuth();

    if(getIsLogueado()){
        return children;
    }else{
        return <Navigate to="/login" />
    }

}

Esta ruta servirá como envoltorio de aquellos componentes que sólo se podrán acceder si el usuario está logueado.

<Navigate /> es un componente que no vimos, pero que le pertenece a react-router-dom. Al encontrarse con éste, se hará una redirección a la url que se indica con el atributo to (to=»/login»)

Por tanto el componente padre que creamos, ProtectedRoute, hará de protección del comente hijo, referenciado como children.

Si el usuario está logueado, retorna children, sino redirige al login:

if(getIsLogueado()){
    return children;
}else{
    return <Navigate to="/login" />
}

Entonces vamos a ir a main.jsx y a modificar:

<Route path='/welcome' element={<Welcome />} />

Por:

<Route path='/welcome' element={
    <ProtectedRoute>
        <Welcome />
    </ProtectedRoute>
} />

(No olvidarse de: import { ProtectedRoute } from ‘./context/ProtectedRoute’ )

Repositorio: https://github.com/fernandoggaitan/mi-primer-proyecto-react