Fase 3: Refactorizado SignalR a un hook reutilizable (useSignalR) y conectado al Dashboard.

This commit is contained in:
2025-10-28 12:26:49 -03:00
parent 7eee798c99
commit 9be62937bd
8 changed files with 347 additions and 54 deletions

View File

@@ -1,7 +1,9 @@
// backend/src/Titulares.Api/Controllers/TitularesController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Titulares.Api.Data;
using Titulares.Api.Hubs;
using Titulares.Api.Models;
namespace Titulares.Api.Controllers;
@@ -11,10 +13,20 @@ namespace Titulares.Api.Controllers;
public class TitularesController : ControllerBase
{
private readonly TitularRepositorio _repositorio;
private readonly IHubContext<TitularesHub> _hubContext;
public TitularesController(TitularRepositorio repositorio)
public TitularesController(TitularRepositorio repositorio, IHubContext<TitularesHub> hubContext)
{
_repositorio = repositorio;
_hubContext = hubContext;
}
private async Task NotificarCambios()
{
var titularesActualizados = await _repositorio.ObtenerTodosAsync();
// Enviamos un mensaje llamado "TitularesActualizados" a TODOS los clientes conectados
// y les pasamos la lista completa y actualizada.
await _hubContext.Clients.All.SendAsync("TitularesActualizados", titularesActualizados);
}
[HttpGet]
@@ -27,40 +39,35 @@ public class TitularesController : ControllerBase
[HttpPost]
public async Task<IActionResult> CrearManual([FromBody] CrearTitularDto titularDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var nuevoId = await _repositorio.CrearManualAsync(titularDto);
await NotificarCambios(); // Notificamos después de crear
return CreatedAtAction(nameof(ObtenerTodos), new { id = nuevoId }, null);
}
[HttpPut("{id}")]
public async Task<IActionResult> Actualizar(int id, [FromBody] ActualizarTitularDto titularDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var resultado = await _repositorio.ActualizarTextoAsync(id, titularDto);
return resultado ? NoContent() : NotFound();
if (!resultado) return NotFound();
await NotificarCambios(); // Notificamos después de actualizar
return NoContent();
}
[HttpPut("reordenar")]
public async Task<IActionResult> Reordenar([FromBody] List<ReordenarTitularDto> ordenes)
{
if (ordenes == null || !ordenes.Any())
{
return BadRequest("La lista de órdenes no puede estar vacía.");
}
var resultado = await _repositorio.ActualizarOrdenAsync(ordenes);
return resultado ? Ok() : StatusCode(500, "Error al actualizar el orden.");
if (!resultado) return StatusCode(500, "Error al actualizar el orden.");
await NotificarCambios(); // Notificamos después de reordenar
return Ok();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Eliminar(int id)
{
var resultado = await _repositorio.EliminarAsync(id);
return resultado ? NoContent() : NotFound();
if (!resultado) return NotFound();
await NotificarCambios(); // Notificamos después de eliminar
return NoContent();
}
}

View File

@@ -0,0 +1,12 @@
// backend/src/Titulares.Api/Hubs/TitularesHub.cs
using Microsoft.AspNetCore.SignalR;
namespace Titulares.Api.Hubs;
// Esta clase es el punto de conexión para los clientes de SignalR.
// No necesitamos añadirle métodos personalizados porque solo enviaremos
// mensajes desde el servidor hacia los clientes.
public class TitularesHub : Hub
{
}

View File

@@ -1,35 +1,34 @@
// backend/src/Titulares.Api/Program.cs
using Titulares.Api.Data;
using Titulares.Api.Hubs; // Añadir este using
var builder = WebApplication.CreateBuilder(args);
// 1. Añadir servicios al contenedor.
// ===================================
// Añadimos los servicios para los controladores API
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Añadimos nuestro repositorio personalizado
builder.Services.AddSingleton<TitularRepositorio>();
// Añadimos los servicios de autorización (necesario para app.UseAuthorization)
builder.Services.AddAuthorization();
// Añadimos la política de CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowReactApp", builder =>
{
builder.WithOrigins("http://localhost:5174")
builder.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddSignalR();
// Añadimos los servicios de autorización (necesario para app.UseAuthorization)
builder.Services.AddAuthorization();
// 2. Construir la aplicación.
// ==========================
var app = builder.Build();
@@ -43,15 +42,21 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// COMENTAMOS LA REDIRECCIÓN HTTPS PORQUE TRABAJAMOS CON HTTP
// app.UseHttpsRedirection();
// Usamos la política de CORS que definimos
// 1. Activa el enrutamiento para que la app sepa a dónde va la petición.
app.UseRouting();
// 2. APLICA LA POLÍTICA DE CORS.
app.UseCors("AllowReactApp");
// Usamos la autorización
// 3. Usamos la autorización.
app.UseAuthorization();
// Mapeamos los controladores para que la API responda a las rutas
// 4. Mapeamos los endpoints (Controladores y Hubs).
app.MapControllers();
app.MapHub<TitularesHub>("/titularesHub");
app.Run();

View File

@@ -13,6 +13,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@microsoft/signalr": "^9.0.6",
"@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4",
"axios": "^1.13.0",
@@ -1217,6 +1218,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@microsoft/signalr": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-9.0.6.tgz",
"integrity": "sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"eventsource": "^2.0.2",
"fetch-cookie": "^2.0.3",
"node-fetch": "^2.6.7",
"ws": "^7.5.10"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.4.tgz",
@@ -2214,6 +2228,18 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2904,6 +2930,24 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2965,6 +3009,16 @@
"reusify": "^1.0.4"
}
},
"node_modules/fetch-cookie": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-2.2.0.tgz",
"integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==",
"license": "Unlicense",
"dependencies": {
"set-cookie-parser": "^2.4.8",
"tough-cookie": "^4.0.0"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3593,6 +3647,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.26",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
@@ -3805,16 +3879,33 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -3889,6 +3980,12 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -4011,6 +4108,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4158,6 +4261,27 @@
"node": ">=8.0"
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -4235,6 +4359,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
@@ -4276,6 +4409,16 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/vite": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
@@ -4382,6 +4525,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4408,6 +4567,27 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -15,6 +15,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@microsoft/signalr": "^9.0.6",
"@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4",
"axios": "^1.13.0",

View File

@@ -1,11 +1,13 @@
// frontend/src/components/Dashboard.tsx
import { useEffect, useState } from 'react';
import { Box, Button, Typography, Stack } from '@mui/material';
import { useEffect, useState, useCallback } from 'react';
import { Box, Button, Typography, Stack, Chip } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import SyncIcon from '@mui/icons-material/Sync';
import type { Titular } from '../types';
import * as api from '../services/apiService';
import { useSignalR } from '../hooks/useSignalR';
import FormularioConfiguracion from './FormularioConfiguracion';
import TablaTitulares from './TablaTitulares';
import AddTitularModal from './AddTitularModal';
@@ -14,30 +16,36 @@ const Dashboard = () => {
const [titulares, setTitulares] = useState<Titular[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const cargarTitulares = async () => {
try {
const data = await api.obtenerTitulares();
setTitulares(data);
} catch (error) {
console.error("Error al cargar titulares:", error);
}
};
// Usamos useCallback para que la función de callback no se recree en cada render,
// evitando que el useEffect del hook se ejecute innecesariamente.
const onTitularesActualizados = useCallback((titularesActualizados: Titular[]) => {
console.log("Datos recibidos desde SignalR:", titularesActualizados);
setTitulares(titularesActualizados);
}, []); // El array vacío significa que esta función nunca cambiará
// Usamos nuestro hook y le pasamos el evento que nos interesa escuchar
const { connectionStatus } = useSignalR([
{ eventName: 'TitularesActualizados', callback: onTitularesActualizados }
]);
// La carga inicial de datos sigue siendo necesaria por si el componente se monta
// antes de que llegue la primera notificación de SignalR.
useEffect(() => {
cargarTitulares();
api.obtenerTitulares()
.then(setTitulares)
.catch(error => console.error("Error al cargar titulares:", error));
}, []);
const handleReorder = async (titularesReordenados: Titular[]) => {
setTitulares(titularesReordenados); // Actualización optimista de la UI
const payload = titularesReordenados.map((item, index) => ({
id: item.id,
nuevoOrden: index
}));
setTitulares(titularesReordenados);
const payload = titularesReordenados.map((item, index) => ({ id: item.id, nuevoOrden: index }));
try {
await api.actualizarOrdenTitulares(payload);
// Ya no necesitamos hacer nada más, SignalR notificará a todos los clientes.
} catch (err) {
console.error("Error al reordenar:", err);
cargarTitulares(); // Revertir en caso de error
// En caso de error, volvemos a pedir los datos para no tener un estado inconsistente.
api.obtenerTitulares().then(setTitulares);
}
};
@@ -45,7 +53,7 @@ const Dashboard = () => {
if (window.confirm('¿Estás seguro de que quieres eliminar este titular?')) {
try {
await api.eliminarTitular(id);
setTitulares(titulares.filter(t => t.id !== id)); // Actualizar UI
// SignalR se encargará de actualizar el estado.
} catch (err) {
console.error("Error al eliminar:", err);
}
@@ -55,18 +63,33 @@ const Dashboard = () => {
const handleAdd = async (texto: string) => {
try {
await api.crearTitularManual(texto);
cargarTitulares(); // Recargar la lista para ver el nuevo titular
// SignalR se encargará de actualizar el estado.
} catch (err) {
console.error("Error al añadir titular:", err);
}
};
const getStatusChip = () => {
switch (connectionStatus) {
case 'Connected':
return <Chip label="Conectado" color="success" size="small" />;
case 'Reconnecting':
case 'Connecting':
return <Chip label="Conectando..." color="warning" size="small" />;
default:
return <Chip label="Desconectado" color="error" size="small" />;
}
}
return (
<>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h4" component="h1">
Titulares Dashboard
</Typography>
<Stack direction="row" spacing={2} alignItems="center">
<Typography variant="h4" component="h1">
Titulares Dashboard
</Typography>
{getStatusChip()}
</Stack>
<Stack direction="row" spacing={2}>
<Button variant="outlined" startIcon={<SyncIcon />}>
Generate CSV
@@ -79,7 +102,6 @@ const Dashboard = () => {
<FormularioConfiguracion />
<TablaTitulares titulares={titulares} onReorder={handleReorder} onDelete={handleDelete} />
<AddTitularModal open={modalOpen} onClose={() => setModalOpen(false)} onAdd={handleAdd} />
</>
);

View File

@@ -0,0 +1,66 @@
// frontend/src/hooks/useSignalR.ts
import { useEffect, useRef, useState } from 'react';
import * as signalR from '@microsoft/signalr';
const HUB_URL = 'http://localhost:5174/titularesHub';
// Definimos un tipo para el estado de la conexión para más claridad
export type ConnectionStatus = 'Connecting' | 'Connected' | 'Disconnected' | 'Reconnecting';
// El hook ahora acepta los listeners como argumentos
export const useSignalR = (listeners: { eventName: string; callback: (...args: any[]) => void }[]) => {
// Usamos useRef para mantener una única instancia de la conexión a través de los re-renders
const connectionRef = useRef<signalR.HubConnection | null>(null);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('Disconnected');
useEffect(() => {
// Si no hay conexión, la creamos.
if (!connectionRef.current) {
connectionRef.current = new signalR.HubConnectionBuilder()
.withUrl(HUB_URL)
.withAutomaticReconnect()
.build();
}
const connection = connectionRef.current;
// Registramos los listeners que nos pasaron como argumento
listeners.forEach(listener => {
connection.on(listener.eventName, listener.callback);
});
// Manejamos los cambios de estado para feedback en la UI
connection.onreconnecting(() => setConnectionStatus('Reconnecting'));
connection.onreconnected(() => setConnectionStatus('Connected'));
connection.onclose(() => setConnectionStatus('Disconnected'));
// Solo iniciamos la conexión si está desconectada
if (connection.state === signalR.HubConnectionState.Disconnected) {
setConnectionStatus('Connecting');
connection.start()
.then(() => {
setConnectionStatus('Connected');
console.log('SignalR Conectado.');
})
.catch(err => {
console.error('Error de conexión con SignalR: ', err);
setConnectionStatus('Disconnected');
});
}
// --- FUNCIÓN DE LIMPIEZA ---
// Esto se ejecuta cuando el componente que usa el hook se desmonta
return () => {
// Quitamos los listeners para evitar fugas de memoria
listeners.forEach(listener => {
connection.off(listener.eventName, listener.callback);
});
// Opcional: podría detener la conexión aquí si solo un componente la usa.
// connection.stop();
};
}, [listeners]); // El efecto se re-ejecutará si la lista de listeners cambia
return { connectionStatus };
};

View File

@@ -3,7 +3,7 @@
import axios from 'axios';
import type { Titular } from '../types';
const API_URL = 'https://localhost:5174/api';
const API_URL = 'http://localhost:5174/api';
const apiClient = axios.create({
baseURL: API_URL,