e
React Router, librerías de UI
La interfaz de usuario de nuestra aplicación es actualmente bastante básica:

Queremos cambiar eso. Comencemos con la estructura de navegación de la aplicación.
Es muy común que las aplicaciones web tengan una barra de navegación que permite a los usuarios cambiar entre diferentes vistas dentro de la aplicación. Nuestra aplicación para tomar notas podría incluir una página de inicio:

y una página separada para ver notas:

así como una página para crear notas:

En una aplicación web tradicional, cambiar entre las páginas mostradas por la aplicación implicaba que el navegador enviara una nueva solicitud HTTP GET al servidor y luego renderizara el código HTML devuelto por el servidor, que correspondía a la nueva vista.
Sin embargo, en las aplicaciones de una sola página, en realidad estás en la misma página todo el tiempo y el código JavaScript ejecutado en el navegador crea la ilusión de diferentes "páginas". Si se realizan solicitudes HTTP al cambiar de vista, se utilizan únicamente para recuperar datos con formato JSON que pueden ser necesarios para mostrar la nueva vista.
Una aplicación con una barra de navegación y múltiples vistas sería fácil de implementar con React, por ejemplo, haciendo que el estado de la aplicación page recuerde en qué página se encuentra el usuario y muestre la vista correcta en función de esto:
const App = () => {
const [page, setPage] = useState('home')
const toPage = (page) => (event) => {
event.preventDefault()
setPage(page)
}
const content = () => {
if (page === 'home') {
return <Home />
} else if (page === 'notes') {
return <Notes />
} else if (page === 'users') {
return <Users />
}
}
return (
<div>
<div>
<a href="" onClick={toPage('home')} >
home
</a>
<a href="" onClick={toPage('notes')}>
notes
</a>
<a href="" onClick={toPage('users')} >
users
</a>
</div>
{content()}
</div>
)
}Sin embargo, este método no es óptimo: la URL del sitio web sigue siendo la misma incluso cuando estás en una vista diferente. Sin embargo, cada vista debe tener su propia URL para que los usuarios puedan, por ejemplo, marcar páginas como favoritas. Además, el botón Atrás del navegador no funciona lógicamente si las páginas no tienen direcciones propias; es decir, hacer clic en el botón Atrás no lo llevará a la vista de la aplicación vista anteriormente, sino a otro lugar completamente diferente.
React Router
Afortunadamente, la librería React Router ofrece una excelente solución para gestionar la navegación en una aplicación React.
Instalemos React Router:
npm install react-router-domCreemos un nuevo componente que sirva como página principal de la aplicación.
const Home = () => {
return (
<div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</div>
)
}
export default HomeExtraeremos la vista principal anterior de la aplicación (que estaba en el componente App) a su propio componente, pero moveremos el manejo del estado de las notas fuera del componente:
// list of notes passed as a parameter
const NoteList = ({ notes }) => { // content mostly the same as in the App component
// reference to NoteForm is removed
}El componente App ahora cambia de la siguiente manera
import { useState, useEffect } from 'react'
import noteService from './services/notes'
import {
BrowserRouter as Router,
Routes, Route, Link
} from 'react-router-dom'
import NoteList from './components/NoteList'
import Home from './components/Home'
import Footer from './components/Footer'
import NoteForm from './components/NoteForm'
const App = () => {
const [notes, setNotes] = useState([])
useEffect(() => {
noteService.getAll().then(initialNotes => {
setNotes(initialNotes)
})
}, [])
const addNote = noteObject => {
noteService.create(noteObject).then(returnedNote => {
setNotes(notes.concat(returnedNote))
})
}
const padding = {
padding: 5
}
return (
<Router> <div> <Link style={padding} to="/">home</Link> <Link style={padding} to="/notes">notes</Link> <Link style={padding} to="/create">new note</Link> </div>
<Routes> <Route path="/notes" element={ <NoteList notes={notes} /> } /> <Route path="/create" element={ <NoteForm createNote={addNote}/> } /> <Route path="/" element={<Home />} /> </Routes> <Footer /> </Router> )
}
export default AppEl enrutamiento —es decir, el renderizado condicional de componentes según la URL del navegador— se habilita colocando los componentes como hijos de Router, dentro de sus etiquetas.
Primero, la barra de navegación de la aplicación se define utilizando el componente Link. El atributo to especifica cómo se cambia la URL del navegador cuando se hace clic en el enlace:
<div>
<Link style={padding} to="/">home</Link>
<Link style={padding} to="/notes">notes</Link>
<Link style={padding} to="/create">new note</Link>
</div>A continuación, el enrutamiento de la aplicación se define utilizando el componente Routes. Dentro del componente usamos Route para definir un conjunto de reglas y los componentes que se renderizarán para cada una:
<Routes>
<Route path="/notes" element={
<NoteList notes={notes} />
} />
<Route path="/create" element={
<NoteForm createNote={addNote}/>
} />
<Route path="/" element={<Home />} />
</Routes>En la URL raíz de la aplicación se renderiza el componente Home:

Al hacer clic en "notas" en la barra de navegación, la dirección del navegador cambia a notes y se renderiza el componente NoteList:

De manera similar, al hacer clic en "nueva nota", la URL pasa a ser create y se renderiza el componente NoteForm.
En una página web normal, cambiar la dirección en la barra de direcciones del navegador hace que la página se vuelva a cargar. Sin embargo, cuando se usa React Router, esto no sucede; en cambio, el enrutamiento se maneja completamente a través de JavaScript en la interfaz.
El componente del enrutador que utilizamos es BrowserRouter:
import {
BrowserRouter as Router, Routes, Route, Link
} from 'react-router-dom'Según la documentación
BrowserRouter es un Router que utiliza la API de historial HTML5 (pushState, replaceState y el evento popstate) para mantener su interfaz de usuario sincronizada con la URL.
BrowserRouter utiliza la API de historial HTML5 para permitir que la URL en la barra de direcciones del navegador se use para el "enrutamiento" interno dentro de una aplicación React, lo que significa que incluso si la URL en la barra de direcciones cambia, el contenido de la página se manipula únicamente a través de JavaScript y el navegador no carga contenido nuevo desde el servidor. Sin embargo, el comportamiento del navegador con respecto a las funciones de avance y retroceso y los marcadores es intuitivo: funciona igual que en los sitios web tradicionales.
El código actual de la aplicación está disponible en su totalidad en GitHub, en la rama part5-10.
Ruta parametrizada
Movamos los detalles de una sola nota a su propia vista, a la que se puede acceder haciendo clic en el nombre de la nota:

La capacidad de hacer clic en el nombre se ha implementado en el componente NoteList de la siguiente manera:
import { Link } from 'react-router-dom'
const NoteList = ({ notes }) => {
// ...
return (
<div>
<h1>Notes</h1>
<Notification message={errorMessage} />
{!user && loginForm()}
<div>
<button onClick={() => setShowAll(!showAll)}>
show {showAll ? 'important' : 'all'}
</button>
</div>
<ul>
{notesToShow.map(note => (
<li key={note.id}>
<Link to={`/notes/${note.id}`}>{note.content}</Link> </li>
))}
</ul>
</div>
)
}
export default NoteListAsí volvemos a utilizar Link. Por ejemplo, al hacer clic en el nombre de una nota cuyo id es 12345, la URL del navegador se actualiza a notes/12345.
La URL parametrizada se define en el enrutamiento dentro del componente App de la siguiente manera:
<Router>
// ...
<Routes>
<Route path="/notes/:id" element={ <Note notes={notes} toggleImportanceOf={toggleImportanceOf} /> } /> <Route path="/notes" element={<Notes notes={notes} />} />
<Route path="/users" element={user ? <Users /> : <Navigate replace to="/login" />} />
<Route path="/login" element={<Login onLogin={login} />} />
<Route path="/" element={<Home />} />
</Routes>
</Router>La ruta que representa la vista para una sola nota se define en el "estilo Express" marcando el parámetro de ruta con la notación :id de la siguiente manera:
<Route path="/notes/:id" element={<Note notes={notes} ... />} />Cuando el navegador navega a la URL única de una nota, por ejemplo, /notes/12345, se representa el componente Note, que ahora hemos tenido que modificar ligeramente:
import { useParams } from 'react-router-dom'
const Note = ({ notes, toggleImportance }) => {
const id = useParams().id const note = notes.find(n => n.id === id)
const label = note.important ? 'make not important' : 'make important'
return (
<li className="note">
<span>{note.content}</span>
<button onClick={() => toggleImportance(id)}>{label}</button>
</li>
)
}
export default NoteA diferencia de antes, el componente Note ahora recibe todas las notas mediante la prop notes y puede acceder a la parte variable de la URL —en concreto, el id de la nota que se mostrará— mediante el hook useParams de React Router.
useNavigate
El backend ya admite la eliminación de notas. Para implementar esto, agreguemos un botón a la página de notas individuales en la aplicación:

Agreguemos un controlador al componente App que realiza la eliminación y pasémoslo al componente Note:
const App = () => {
const deleteNote = (id) => { noteService.remove(id).then(() => { setNotes(notes.filter(n => n.id !== id)) }) }
return (
// ...
<Routes>
<Route path="/notes/:id" element={
<Note
notes={notes}
toggleImportanceOf={toggleImportanceOf}
deleteNote={deleteNote} />
} />
<Route path="/notes" element={
<NoteList notes={notes} />
} />
<Route path="/create" element={
<NoteForm createNote={addNote}/>
} />
<Route path="/" element={<Home />} />
</Routes>
<Footer />
</Router>
)
} El componente Note cambia de la siguiente manera:
import { useParams, useNavigate } from 'react-router-dom'
const Note = ({ notes, toggleImportanceOf, deleteNote }) => { const id = useParams().id
const navigate = useNavigate() const note = notes.find(n => n.id === id)
const label = note.important ? 'make not important' : 'make important'
const handleDelete = () => { if (window.confirm(`Delete note "${note.content}"?`)) { deleteNote(id) navigate('/notes') } }
return (
<li className="note">
<span>{note.content}</span>
<button onClick={() => toggleImportanceOf(id)}>{label}</button>
<button onClick={handleDelete}>delete</button> </li>
)
}
export default NoteCuando se elimina una nota, el usuario regresa a la página que enumera todas las notas. Esto se hace llamando a la función devuelta por useNavigate de React Router con la URL deseada: navigate('/notes').
Las funciones useParams y useNavigate de React Router son hooks, al igual que useState y useEffect, que ya hemos utilizado muchas veces. Como vimos en la parte 1, existen ciertas reglas asociadas al uso de hooks.
Modifiquemos también el componente NoteForm para que después de agregar una nueva nota, el usuario acceda a la página que contiene todas las notas:
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
const NoteForm = ({ createNote }) => {
const [newNote, setNewNote] = useState('')
const navigate = useNavigate()
const addNote = event => {
event.preventDefault()
createNote({
content: newNote,
important: true
})
navigate('/notes') setNewNote('')
}
return (
<div>
<h2>Create a new note</h2>
<form onSubmit={addNote}>
<input
value={newNote}
onChange={event => setNewNote(event.target.value)}
placeholder="write note content here"
/>
<button type="submit">save</button>
</form>
</div>
)
}Ruta parametrizada revisada
Hay un problema ligeramente molesto con la aplicación. El componente Note recibe all notes como props, aunque solo muestra aquella cuyo id coincide con la parte parametrizada de la URL:
const Note = ({ notes, toggleImportance }) => {
const id = useParams().id
const note = notes.find(n => n.id === Number(id))
// ...
}¿Sería posible modificar la aplicación para que Note reciba solo la nota que se mostrará como prop?
import { useParams, useNavigate } from 'react-router-dom'
const Note = ({ note, id, toggleImportanceOf, deleteNote }) => { const id = useParams().id
const navigate = useNavigate()
// ...
return (
<li className="note">
<span>{note.content}</span>
<button onClick={() => toggleImportanceOf(id)}>{label}</button>
<button onClick={handleDelete}>delete</button>
</li>
)
}
export default NoteUna posibilidad es determinar dentro del componente el id de la nota que se mostrará mediante el hook useMatch de React Router.
No es posible utilizar el hook useMatch en el mismo componente que define la parte enrutable de la aplicación. Movamos el componente Router fuera de App:
ReactDOM.createRoot(document.getElementById('root')).render(
<Router> <App />
</Router>)El componente App se convierte en:
import {
// ...
useMatch} from 'react-router-dom'
const App = () => {
// ...
const match = useMatch('/notes/:id') const note = match ? notes.find(note => note.id === match.params.id) : null
return (
<div>
<div>
<Link style={padding} to="/">home</Link>
// ...
</div>
<Routes>
<Route path="/notes/:id" element={
<Note
note={note} toggleImportanceOf={toggleImportanceOf}
deleteNote={deleteNote}
/>
} />
<Route path="/notes" element={
<NoteList notes={notes} />
} />
<Route path="/create" element={
<NoteForm createNote={addNote}/>
} />
<Route path="/" element={<Home />} />
</Routes>
<div>
<em>Note app, Department of Computer Science 2026</em>
</div>
</div>
)
} Cada vez que se renderiza el componente App (lo que, en la práctica, ocurre cada vez que cambia la URL en la barra de direcciones del navegador) se ejecuta el siguiente comando
const match = useMatch('/notes/:id')Si la URL tiene el formato /notes/:id, es decir, corresponde a la URL de una sola nota, a la variable match se le asigna un objeto que puede usarse para determinar la parte parametrizada de la ruta, es decir, el id de la nota. Esto nos permite recuperar la nota a renderizar:
const note = match
? notes.find(note => note.id === match.params.id)
: nullTodavía hay un pequeño error en nuestra aplicación. Si el navegador se recarga en una sola página de notas, se produce un error:

El problema surge porque se intenta representar la página antes de que se hayan obtenido las notas del backend. Podemos resolver este problema con renderizado condicional:
const Note = ({ note, toggleImportanceOf, deleteNote }) => {
const id = useParams().id
const navigate = useNavigate()
if(!note) { return null }
return (
//...
)
}La aplicación tiene una característica más molesta: la lógica de inicio de sesión todavía está en la página que enumera las notas. Sin embargo, dejaremos la funcionalidad en este estado algo incompleto por ahora.
El código actual de la aplicación está disponible en su totalidad en GitHub, en la rama part5-11.
Librerías de UI
En la parte 2 ya vimos dos formas de añadir estilos: mediante un único archivo CSS tradicional y mediante estilos en línea. En esta sección veremos algunas formas más.
Una forma de definir los estilos de una aplicación es utilizar un framework de UI, es decir, una librería de estilos para interfaces de usuario.
El primer framework de UI que alcanzó una gran popularidad fue Bootstrap, desarrollado por Twitter. Durante los últimos años, los frameworks de UI han proliferado. La selección es tan amplia que ni siquiera merece la pena intentar enumerarlos todos aquí.
Muchos marcos de UI incluyen temas predefinidos para aplicaciones web, así como "componentes", como botones, menús y tablas. El término "componente" está escrito entre comillas arriba porque no se refiere a lo mismo que un componente de React. La mayoría de las veces, los marcos de UI se utilizan incluyendo las hojas de estilo CSS del marco y el código JavaScript en la aplicación.
Muchos marcos de UI se han adaptado a versiones compatibles con React, donde los "componentes" definidos por el marco de UI se han convertido en componentes de React. Por ejemplo, hay un par de versiones React de Bootstrap, la más popular de las cuales es React-Bootstrap.
En lugar de Bootstrap, veamos el que quizá sea actualmente el framework de UI más popular: la librería para React Material UI, que implementa el lenguaje de diseño Material Design de Google.
Instalemos la librería:
npm install @mui/material @emotion/react @emotion/styledCuando se utiliza Material UI, todo el contenido de la aplicación suele renderizarse dentro del componente Container:
import { Container } from '@mui/material'
const App = () => {
// ...
return (
<Container>
// ...
</Container>
)
}Tabla
Comencemos con el componente NoteList y rendericemos la lista de notas como una tabla, que también muestra el usuario que creó cada nota:
import { useState, useEffect } from 'react'
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from '@mui/material'
//...
const NoteList = ({ notes }) => {
// ...
return (
<div>
// ...
<h2>Notes</h2>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>content</TableCell>
<TableCell>user</TableCell>
<TableCell>important</TableCell>
</TableRow>
</TableHead>
<TableBody>
{notes.map(note => (
<TableRow key={note.id}>
<TableCell>
<Link to={`/notes/${note.id}`}>
{note.content}
</Link>
</TableCell>
<TableCell>
{note.user.name}
</TableCell>
<TableCell>
{note.important ? 'yes': ''}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</div>
)
}
export default NoteListLa tabla se ve así:

Formulario
A continuación, mejoremos la vista para crear una nueva nota NoteForm usando los componentes TextField y Button:
import { TextField, Button } from '@mui/material'
// ...
const NoteForm = ({ createNote }) => {
// ...
return (
<div>
<h2>Create a new note</h2>
<form onSubmit={addNote}>
<TextField
label="note content"
value={newNote}
onChange={event => setNewNote(event.target.value)}
/>
<div>
<Button type="submit" variant="contained" style={{ marginTop: 10 }}>
save
</Button>
</div>
</form>
</div>
)
}
export default NoteFormEl resultado es elegante:

Notificaciones
Mejoremos el componente de notificación mediante el componente Alert de Material UI:
import { Alert } from '@mui/material'
const Notification = ({ notification }) => {
if (notification === null) {
return null
}
return (
<Alert style={{ marginTop: 10, marginBottom: 10 }} severity={notification.type}>
{notification.text}
</Alert>
)
}
export default NotificationMueva el componente de notificación y su gestión de estado al componente App:
const App = () => {
const [notes, setNotes] = useState([])
const [notification, setNotification] = useState(null)
// ...
const addNote = noteObject => {
noteService.create(noteObject).then(returnedNote => {
setNotes(notes.concat(returnedNote))
setNotification({ text: `Note '${returnedNote.content}' added!`, type: 'success' }) setTimeout(() => {
setNotification(null)
}, 5000)
})
}
return (
<Container>
<div>
<Link style={padding} to="/">home</Link>
<Link style={padding} to="/notes">notes</Link>
<Link style={padding} to="/create">new note</Link>
</div>
<Notification notification={notification} />
<Routes>
<Route path="/notes/:id" element={
<Note
note={note}
toggleImportanceOf={toggleImportanceOf}
deleteNote={deleteNote}
/>
} />
<Route path="/notes" element={
<NoteList notes={notes} setNotification={setNotification} />
} />
<Route path="/create" element={
<NoteForm createNote={addNote} />
} />
<Route path="/" element={<Home />} />
</Routes>
<Footer />
</Container>
)
}Alert tiene un diseño elegante:

Menú de navegación
El menú de navegación se implementa utilizando el componente AppBar.
Si aplicamos el ejemplo de la documentación directamente
<AppBar position="static">
<Toolbar>
<Button color="inherit"><Link to="/">home</Link></Button>
<Button color="inherit"><Link to="/notes">notes</Link></Button>
<Button color="inherit"><Link to="/create">new note</Link></Button>
</Toolbar>
</AppBar>Esto proporciona una solución funcional, pero su apariencia no es la mejor posible:

En la documentación encontramos una alternativa mejor: la prop component, que permite cambiar cómo se renderiza el elemento raíz de un componente de Material UI.
Al definir
<Button color="inherit" component={Link} to="/">
home
</Button>El componente Button se renderiza usando como raíz el componente Link de react-router-dom, al que se pasa la prop to que especifica la ruta.
El código completo para la barra de navegación es el siguiente
<AppBar position="static">
<Toolbar>
<Button color="inherit" component={Link} to="/">home</Button>
<Button color="inherit" component={Link} to="/notes">notes</Button>
<Button color="inherit" component={Link} to="/create">new note</Button>
</Toolbar>
</AppBar>y el resultado se ve tal como queremos:

Sin embargo, notamos que cuando se mueve el mouse sobre la barra de navegación, el indicador de desplazamiento es demasiado sutil. Arreglemos esto definiendo un color de fondo ligeramente mejor para estas situaciones:
const style = { '&:hover': { bgcolor: 'rgba(255,255,255,0.3)' } }
return (
<Container>
<AppBar position="static">
<Toolbar>
<Button color="inherit" component={Link} to="/" sx={style}>
home
</Button>
<Button color="inherit" component={Link} to="/notes" sx={style}>
notes
</Button>
<Button color="inherit" component={Link} to="/create" sx={style}>
new note
</Button>
</Toolbar>
</AppBar>
// ...
)Finalmente estamos satisfechos:

El código actual de la aplicación está disponible en su totalidad en GitHub, en la rama part5-12.
Styled Components
Además de lo que ya hemos visto, existen otras formas de aplicar estilos a una aplicación React.
La librería styled-components, que utiliza la sintaxis literal de plantilla etiquetada de ES6, ofrece un enfoque interesante para definir estilos.
Instalemos Styled Components y utilicémoslo para realizar algunos cambios de estilo en la aplicación de notas (la versión anterior a la instalación de Material UI). Primero, creemos dos definiciones de estilo para los componentes que usaremos:
import styled from 'styled-components'
const Button = styled.button`
background: Bisque;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid Chocolate;
border-radius: 3px;
`
const Input = styled.input`
margin: 0.25em;
width: 300px;
`El código crea versiones de los elementos HTML button y input con estilo y los asigna a las variables Button y Input.
La sintaxis para definir estilos es bastante interesante, ya que las definiciones CSS se colocan entre comillas invertidas. Esta es la sintaxis de los literales de plantilla etiquetados de ES6.
Los componentes definidos funcionan como elementos normales button y input, y se utilizan en la aplicación de la forma habitual:
const NoteForm = ({ createNote }) => {
// ...
return (
<div>
<h2>Create a new note</h2>
<form onSubmit={addNote}>
<Input> value={newNote}
onChange={event => setNewNote(event.target.value)}
placeholder="write note content here"
/>
<Button type="submit">save</Button> </form>
</div>
)
}El formulario ahora se ve así:

Definamos los siguientes componentes para agregar estilos, todos los cuales son versiones mejoradas de los elementos div:
const Page = styled.div`
padding: 1em;
background: papayawhip;
`
const Navigation = styled.div`
background: BurlyWood;
padding: 1em;
`
const Footer = styled.div`
background: Chocolate;
padding: 1em;
margin-top: 1em;
`Los nuevos componentes ahora se pueden utilizar en la aplicación:
const App = () => {
// ...
return (
<Page> <Navigation> <Link style={padding} to="/">home</Link>
<Link style={padding} to="/notes">notes</Link>
<Link style={padding} to="/create">new note</Link>
</Navigation>
<Routes>
<Route path="/notes/:id" element={
<Note
note={note}
toggleImportanceOf={toggleImportanceOf}
deleteNote={deleteNote}
/>
} />
<Route path="/notes" element={
<NoteList notes={notes} />
} />
<Route path="/create" element={
<NoteForm createNote={addNote}/>
} />
<Route path="/" element={<Home />} />
</Routes>
<Footer> Note app, Department of Computer Science, University of Helsinki 2026 </Footer> </Page> )
}El resultado final es el siguiente:

Styled-Components ha ido ganando popularidad últimamente y actualmente parece que mucha gente lo considera la mejor manera de definir estilos para aplicaciones React.









