Rutas Protegias en React Router v6
Cuando creamos aplicaciones de React eventualmente vamos a querer crear rutas que determinados usuarios puedan ver, solo si estan autenticados en nuestra aplicacion o si tienen determinado rol.
De lo que estamos hablando es de tener rutas privadas o tambien llamadas rutas protegidas.
Asi que en este tutorial aprenderas como proteger Rutas usando React Router en su ultima version 6 (react-router-dom@6).
Antes de empezar es importante aclarar que este es un tutorial de Rutas protegidas en react router y no de como crear tokens, o cookies, o bases de datos.
Este es un tutorial de como entender el enrutador y de como proteger rutas desde el frontend.
Requerimientos
Antes de que empieces a seguir este tutorial, es necesario tener bases en React y React Router DOM. Asi que te dejo los 2 tutoriales de React y React Router DOM:
- Curso de React: https://youtu.be/rLoWMU4L_qE
- Curso de React Router DOM: https://youtu.be/7xRVnmWcTE8
Creacion del proyecto
Para este ejemplo usaremos Vite para crear nuestro proyecto de React:
npm create vite
Esto son las opciones que escogerás para vite:
- Nombre del proyecto: react-router-protected
- Framework: React
- Lenguaje: Javascript
cd react-router-protected
npm i
npm run dev
Luego simplemente quita todo el contenido de App.js, y deja limpio los archivos de CSS, y el proyecto ya estaria listo para empezar.
Creacion de Rutas basicas
Primero crearemos unas cuantas rutas y una navegacion basica para poder cambiar de pagina.
import React from "react";
import { BrowserRouter, Route, Routes, Link } from "react-router-dom";
import { Admin, Analytics, Dashboard, Home, Landing } from "./pages";
function App() {
return (
<BrowserRouter>
<Navigation />
<Routes>
<Route index element={<Landing />} />
<Route path="/landing" element={<Landing />} />
<Route path="/home" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/admin" element={<Admin />} />
</Routes>
</BrowserRouter>
);
}
function Navigation() {
return (
<nav>
<ul>
<li>
<Link to="/landing">Landing</Link>
</li>
<li>
<Link to="/home">Home</Link>
</li>
<li>
<Link to="/dashboard">Dashboard</Link>
</li>
<li>
<Link to="/analytics">Analytics</Link>
</li>
<li>
<Link to="/admin">Admin</Link>
</li>
</ul>
</nav>
);
}
export default App;
Las paginas lucen de esta forma:
export const Landing = () => <h2>Landing Page (Public)</h2>
export const Home = () => <h2>Home Page (Private)</h2>
export const Dashboard = () => <h2>Dashboard (Private)</h2>
export const Analytics = () => <h2>Profile (Private & permission 'analize')</h2>
export const Admin = () => <h2>Admin (Private & permission 'admin')</h2>
Simulando Login y Logout
El poder autenticar un usuario consiste en usar codigo de Backend, ese es un tema mucho mas alla de React Router, asi que lo que vamos a hacer es simular el proceso de hacer Login y Logout, de un usuario usando simplemente un estado de React que podremos cambiar.
function App() {
const [user, setUser] = useState(null);
const login = () => setUser({ id: 1, name: "John" });
const logout = () => setUser(null);
return (
<BrowserRouter>
<Navigation />
{user ? (
<button onClick={logout}>Logout</button>
) : (
<button onClick={login}>Login</button>
)}
<Routes>
....
</Routes>
</BrowserRouter>
);
}
export default App;
Protected Routes
Una forma muy sencilla de proteger rutas de React Router Dom es simplemente colocar una condicional y confirmar si existe un dato, si existe continua mostrando el componente, pero sino existe lo redireccionamos a otro lugar. Por ejemplo:
<Route path="/home" element={<Home user={user} />} />
Luego puedes actualizar el componente Home:
import { Navigate } from "react-router-dom";
...
export const Home = ({ user }) => {
if (!user) {
return <Navigate to="/landing" />;
}
return <h2>Home Page (Private)</h2>;
};
Este enfoque funciona pero no escala muy bien, porque tenemos que implementarlo en multiples paginas. Ademas el componente redirect no debe residir dentro del componente sino desde fuera del componente.
entonces una solucion es crear un componente que solo se encargue de proteger rutas, y poder reutilizarlo.
// components/ProtectedRoute.js
import { Navigate } from "react-router-dom";
export const ProtectedRoute = ({ user, children }) => {
if (!user) {
return <Navigate to="/landing" />;
}
return children;
};
Ahora usando este componente podemos usarlo desde fuera, del componente que queremos proteger de esta forma:
<Route
path="/home"
element={
<ProtectedRoute user={user}>
<Home/>
</ProtectedRoute>
}
/>
Al separar este componente podemos abstraer toda la complejidad en una sola pieza reusable. ademas de permitir extender su funcionalidad decidiendo a donde queremos redireccionar
import { Navigate } from "react-router-dom";
export const ProtectedRoute = ({ user, redirectTo = "/landing", children }) => {
if (!user) {
return <Navigate to={redirectTo} />;
}
return children;
};
Multiples Rutas
protegamos ahora tambien la ruta dashboard:
<Route
path="/dashboard"
element={
<ProtectedRoute user={user}>
<Dashboard />
ahora esto nos lleva a que dos rutas, la ruta /home y /dashboard usen el mismo componente
<Route
path="/home"
element={
<ProtectedRoute user={user}>
<Home />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute user={user}>
<Dashboard />
</ProtectedRoute>
}
/>
sin embargo una mejor forma de proteger multiples rutas hermanas con el mismo nivel de autorizacion es creando una ruta layout la cual renderize el mismo componente ProetectRoute, para ambas rutas.
import { Navigate, Outlet } from "react-router-dom";
export const ProtectedRoute = ({ user, redirectTo = "/landing", children }) => {
if (!user) {
return <Navigate to={redirectTo} />;
}
return <Outlet />;
};
entonces ahora puedes hacer esto:
<Route element={<ProtectedRoute user={user} />}>
<Route path="/home" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Route>
By using React Router's Outlet component instead of React's children prop, you can use the ProtectedRoute component as Layout component. However, when attempting to use the ProtectedRoute as wrapping component as before your application will break. Therefore, you can optionally render the children when the ProtectedRoute is not used as Layout component:
import { Navigate, Outlet } from "react-router-dom";
export const ProtectedRoute = ({ user, redirectTo = "/landing", children }) => {
if (!user) {
return <Navigate to={redirectTo} />;
}
return children ? children : <Outlet />;
};
Roles y otras propidades
That's it for the essential protection of private routes which covers the essential case of having an authenticated user. However, in a more complex application you will encounter permissions and roles too. We will simulate both cases by giving our user a permission and role in arrays, because they could have multiple of them:
const login = () =>
setUser({
id: 1,
name: "John",
permissions: ["analize"],
roles: ["admin"],
});
So far, the ProtectedRoute component only deals with authenticated users as authorization process. We need to extend it to handle permissions and roles too. Therefore, we will enable developers to pass in a boolean as condition which acts as more abstract guard for rendering the protected component:
import { Navigate, Outlet } from "react-router-dom";
export const ProtectedRoute = ({
isAllowed,
redirectTo = "/landing",
children,
}) => {
if (!isAllowed) {
return <Navigate to={redirectTo} replace />;
}
return children ? children : <Outlet />;
};
Because we defined this condition previously in the ProtectedRoute component itself, we need to define the condition from the outside now. This applies to our so far protected routes in addition to the new protected routes which require the user to have a certain permission or role:
<Route
path="/analytics"
element={
<ProtectedRoute
redirectTo="/home"
isAllowed={!!user && user.permissions.includes("analize")}
>
<Analytics />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute
redirectTo="/home"
isAllowed={!!user && user.roles.includes("admin")}
>
<Admin />
</ProtectedRoute>
}
/>
While the Home and Dashboard pages require a user to be present (read: authenticated), the Analytics and Admin pages require a user to be authenticated and to have certain permissions/roles. Try it yourself by revoking the user either their roles or permissions.
Furthermore, the protected route for the Analytics and Admin pages makes use of the optional redirectPath. If a user does not fulfil the permissions or roles authorization requirements, the user gets redirected to the protected Home page. If there is a user that's not authenticated in the first place, they get redirected to the Landing page.
export const Home = () => <h2>Home Page (Private)</h2>;
Más Recursos
- Tutorial de Rutas Protegidas, https://www.robinwieruch.de/react-router-private-routes/ https://www.makeuseof.com/create-protected-route-in-react/
- https://medium.com/@rajputpraj/protected-route-username-in-react-nextjs-part-1-73ccbc200aae
- https://shipsaas.com/blog/create-protected-route-nextjs
- https://dev.to/seven/how-to-implement-protected-routes-in-nextjs-1m50
- https://alexsidorenko.com/blog/next-js-protected-routes/