Qué es resource() y diferencia con toSignal()
La función resource() representa un salto evolutivo en el manejo de datos asíncronos en Angular 19+, diseñada específicamente para conectar el mundo reactivo de los signals con operaciones asíncronas como peticiones HTTP. A diferencia de toSignal() que convierte observables existentes a signals, resource() está pensado desde el principio para gestionar el ciclo completo de vida de datos asíncronos.
¿Qué es resource()?
Resource() es una función declarativa que crea un signal especializado para manejar datos asíncronos. Su principal ventaja es que encapsula automáticamente los tres estados fundamentales de cualquier operación asíncrona: loading, success y error. Esto elimina la necesidad de manejar manualmente estos estados como haríamos con observables tradicionales.
import { resource } from '@angular/core';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export class UserService {
private http = inject(HttpClient);
// Resource para cargar datos de usuario
userData = resource({
request: () => ({ userId: 123 }),
loader: ({ request }) => this.http.get(`/api/users/${request.userId}`)
});
}
Diferencias clave con toSignal()
La distinción fundamental entre resource() y toSignal() radica en su propósito y funcionalidad:
toSignal() es una utilidad de interoperabilidad que toma un observable existente y lo convierte a signal, manteniendo el patrón reactivo pero sin añadir funcionalidades específicas para datos asíncronos:
// Enfoque con toSignal()
export class ProductService {
private http = inject(HttpClient);
// Convertimos un observable a signal
products = toSignal(
this.http.get<Product[]>('/api/products'),
{ initialValue: [] }
);
}
resource(), por el contrario, es una solución completa para datos asíncronos que proporciona:
- Estados automáticos: loading, value, error sin configuración manual
- Reactividad a parámetros: se recarga automáticamente cuando cambian los inputs
- Control de ciclo de vida: métodos reload() y update() integrados
- Optimizaciones: caché automático y prevención de requests duplicados
// Enfoque con resource()
export class ProductService {
private http = inject(HttpClient);
products = resource({
request: () => ({}),
loader: () => this.http.get<Product[]>('/api/products')
});
// Los estados están disponibles automáticamente:
// products.loading(), products.value(), products.error()
}
Ventajas arquitectónicas
Resource() introduce un patrón declarativo que simplifica significativamente el código:
- Menos boilerplate: No necesitas manejar manualmente estados de loading y error
- Mejor UX: Estados consistentes para mostrar spinners, errores y contenido
- Reactividad automática: Se actualiza cuando cambian las dependencias
- Integración nativa: Diseñado específicamente para el ecosistema de signals
Esta aproximación hace que resource() sea la opción preferida para nuevos desarrollos que requieran manejo de datos asíncronos, mientras que toSignal() sigue siendo útil para migrar código existente basado en observables.
loading, value, error, equal, injector
La Resource API expone un conjunto de propiedades y configuraciones que permiten un control granular sobre el comportamiento y estado de los datos asíncronos. Estas propiedades proporcionan tanto acceso a los estados actuales como configuración avanzada del resource.
Estados reactivos del resource
Cada resource creado expone automáticamente tres signals de estado que reflejan el ciclo de vida de la operación asíncrona:
loading()
El signal loading() indica si la operación asíncrona está en progreso. Este estado es fundamental para mostrar indicadores de carga en la interfaz:
@Component({
template: `
@if (userResource.loading()) {
<div class="spinner">Cargando usuario...</div>
}
@if (userResource.value()) {
<div class="user-info">
<h2>{{ userResource.value()!.name }}</h2>
<p>{{ userResource.value()!.email }}</p>
</div>
}
`
})
export class UserProfileComponent {
private http = inject(HttpClient);
private userId = input.required<number>();
userResource = resource({
request: () => ({ id: this.userId() }),
loader: ({ request }) => this.http.get<User>(`/api/users/${request.id}`)
});
}
value()
El signal value() contiene el resultado exitoso de la operación asíncrona. Su tipo corresponde al tipo de retorno del loader, y será undefined
mientras la operación esté en curso o haya fallado:
export class ProductListComponent {
private productService = inject(ProductService);
products = resource({
request: () => ({ category: 'electronics' }),
loader: ({ request }) =>
this.productService.getProductsByCategory(request.category)
});
// Computed signal derivado del value
productCount = computed(() => this.products.value()?.length ?? 0);
// Effect que reacciona a cambios en value
logProducts = effect(() => {
const products = this.products.value();
if (products) {
console.log(`Cargados ${products.length} productos`);
}
});
}
error()
El signal error() contiene cualquier error que haya ocurrido durante la ejecución del loader. Permite manejar errores de forma reactiva:
@Component({
template: `
@if (dataResource.error()) {
<div class="error-message">
<h3>Error al cargar datos</h3>
<p>{{ getErrorMessage() }}</p>
<button (click)="dataResource.reload()">Reintentar</button>
</div>
}
`
})
export class DataViewComponent {
private api = inject(ApiService);
dataResource = resource({
request: () => ({}),
loader: () => this.api.getData()
});
getErrorMessage(): string {
const error = this.dataResource.error();
if (error instanceof HttpErrorResponse) {
return `Error ${error.status}: ${error.message}`;
}
return 'Error desconocido';
}
}
Configuración equal para optimización
La propiedad equal permite definir una función personalizada para comparar requests y evitar recargas innecesarias. Por defecto, resource() usa comparación por referencia:
export class SearchComponent {
private searchService = inject(SearchService);
searchTerm = signal('');
filters = signal({ category: 'all', minPrice: 0 });
searchResults = resource({
request: () => ({
term: this.searchTerm(),
filters: this.filters()
}),
loader: ({ request }) =>
this.searchService.search(request.term, request.filters),
// Comparación personalizada para evitar requests duplicados
equal: (a, b) =>
a?.term === b?.term &&
a?.filters.category === b?.filters.category &&
a?.filters.minPrice === b?.filters.minPrice
});
}
Sin la función equal personalizada, cualquier cambio en el objeto de request (aunque los valores sean iguales) triggearía una nueva carga. La función equal optimiza esto comparando los valores relevantes.
Configuración injector para contexto de inyección
La propiedad injector especifica el contexto de inyección donde se ejecutará el loader. Esto es especialmente útil cuando se crea el resource fuera del contexto de inyección normal:
export class DynamicDataService {
createUserResource(injector: Injector, userId: number) {
return resource({
request: () => ({ userId }),
loader: ({ request }) => {
// El HttpClient se inyecta usando el injector proporcionado
const http = injector.get(HttpClient);
return http.get<User>(`/api/users/${request.userId}`);
},
injector // Contexto de inyección explícito
});
}
}
@Component({
template: `
@for (userResource of userResources; track userResource) {
@if (userResource.value()) {
<div class="user-card">{{ userResource.value()!.name }}</div>
}
}
`
})
export class UserListComponent {
private injector = inject(Injector);
private dataService = inject(DynamicDataService);
userResources = [1, 2, 3].map(userId =>
this.dataService.createUserResource(this.injector, userId)
);
}
Patrón de uso combinado
En aplicaciones reales, estas propiedades trabajan conjuntamente para crear experiencias de usuario robustas:
@Component({
template: `
<div class="data-container">
@if (apiResource.loading()) {
<div class="loading-overlay">
<div class="spinner"></div>
<p>Cargando datos...</p>
</div>
}
@if (apiResource.error() && !apiResource.loading()) {
<div class="error-state">
<p>{{ getErrorMessage() }}</p>
<button (click)="apiResource.reload()" class="retry-btn">
Reintentar
</button>
</div>
}
@if (apiResource.value() && !apiResource.loading()) {
<div class="data-content">
<!-- Contenido exitoso -->
</div>
}
</div>
`
})
export class RobustDataComponent {
private injector = inject(Injector);
private requestParams = signal({ page: 1, limit: 10 });
apiResource = resource({
request: () => this.requestParams(),
loader: ({ request }) => {
const http = this.injector.get(HttpClient);
return http.get(`/api/data?page=${request.page}&limit=${request.limit}`);
},
equal: (a, b) => a?.page === b?.page && a?.limit === b?.limit,
injector: this.injector
});
getErrorMessage(): string {
return this.apiResource.error()?.message ?? 'Error desconocido';
}
}
Estas propiedades proporcionan un control completo sobre el comportamiento del resource, permitiendo crear interfaces de usuario que respondan adecuadamente a todos los estados posibles de las operaciones asíncronas.
Métodos reload() y update()
La Resource API proporciona dos métodos fundamentales para controlar el ciclo de vida y actualización de los datos: reload() y update(). Estos métodos ofrecen diferentes estrategias para refrescar y modificar el estado del resource según las necesidades específicas de la aplicación.
El método reload()
El método reload() fuerza una nueva ejecución del loader, ignorando cualquier caché o estado previo. Es especialmente útil cuando necesitamos refrescar los datos debido a cambios externos o acciones del usuario:
@Component({
template: `
<div class="product-list">
@if (products.loading()) {
<div class="loading">Cargando productos...</div>
}
@if (products.value()) {
<div class="products">
@for (product of products.value()!; track product.id) {
<div class="product-card">{{ product.name }}</div>
}
</div>
}
<button (click)="refreshProducts()" [disabled]="products.loading()">
Actualizar productos
</button>
</div>
`
})
export class ProductCatalogComponent {
private http = inject(HttpClient);
products = resource({
request: () => ({ category: 'electronics' }),
loader: ({ request }) =>
this.http.get<Product[]>(`/api/products?category=${request.category}`)
});
refreshProducts() {
// Fuerza una nueva carga independientemente del estado actual
this.products.reload();
}
}
El reload() es particularmente valioso en escenarios donde los datos pueden haber cambiado en el servidor y necesitamos sincronizar el estado local:
export class NotificationCenterComponent {
private notificationService = inject(NotificationService);
notifications = resource({
request: () => ({}),
loader: () => this.notificationService.getUnreadNotifications()
});
// Recargar cuando el usuario hace clic en "marcar como leída"
async markAsRead(notificationId: string) {
await this.notificationService.markAsRead(notificationId);
// Recarga para obtener la lista actualizada
this.notifications.reload();
}
// Recargar periódicamente
ngOnInit() {
setInterval(() => {
if (!this.notifications.loading()) {
this.notifications.reload();
}
}, 30000); // Cada 30 segundos
}
}
El método update()
El método update() permite modificar el valor actual del resource sin ejecutar el loader. Esto es útil para optimizaciones optimistas o cuando conocemos el nuevo estado sin necesidad de hacer una petición al servidor:
export class TodoListComponent {
private todoService = inject(TodoService);
todos = resource({
request: () => ({}),
loader: () => this.todoService.getAllTodos()
});
async toggleTodo(todoId: string) {
const currentTodos = this.todos.value();
if (!currentTodos) return;
// Actualización optimista: modificamos el estado local inmediatamente
const updatedTodos = currentTodos.map(todo =>
todo.id === todoId
? { ...todo, completed: !todo.completed }
: todo
);
// Actualizamos el resource sin hacer petición
this.todos.update(updatedTodos);
try {
// Enviamos el cambio al servidor en segundo plano
await this.todoService.toggleTodo(todoId);
} catch (error) {
// Si falla, recargamos para sincronizar con el servidor
this.todos.reload();
}
}
}
Diferencias entre reload() y update()
Las diferencias clave entre ambos métodos determinan cuándo usar cada uno:
| Aspecto | reload() | update() | |---------|----------|----------| | Ejecución del loader | Sí, siempre ejecuta el loader | No, modifica el valor directamente | | Estados de loading | Activa loading() durante la ejecución | No afecta al estado loading() | | Uso de red | Realiza petición HTTP | Solo actualiza estado local | | Casos de uso | Sincronizar con servidor | Actualizaciones optimistas |
export class ShoppingCartComponent {
private cartService = inject(CartService);
cart = resource({
request: () => ({}),
loader: () => this.cartService.getCart()
});
// Usar update() para cambios locales inmediatos
increaseQuantity(itemId: string) {
const currentCart = this.cart.value();
if (!currentCart) return;
const updatedCart = {
...currentCart,
items: currentCart.items.map(item =>
item.id === itemId
? { ...item, quantity: item.quantity + 1 }
: item
)
};
// Actualización inmediata sin loading
this.cart.update(updatedCart);
// Sincronizar con servidor de forma asíncrona
this.cartService.updateItem(itemId, updatedCart.items.find(i => i.id === itemId)!);
}
// Usar reload() para sincronizar completamente
syncWithServer() {
// Fuerza una sincronización completa
this.cart.reload();
}
}
Patrones avanzados de uso
Los métodos reload() y update() pueden combinarse para crear patrones sofisticados de gestión de estado:
export class MessagingComponent {
private messageService = inject(MessageService);
private userId = input.required<string>();
messages = resource({
request: () => ({ userId: this.userId() }),
loader: ({ request }) =>
this.messageService.getMessages(request.userId)
});
async sendMessage(content: string) {
const currentMessages = this.messages.value() ?? [];
// 1. Crear mensaje temporal para mostrar inmediatamente
const tempMessage = {
id: `temp-${Date.now()}`,
content,
timestamp: new Date(),
status: 'sending' as const
};
// 2. Actualizar localmente para feedback inmediato
this.messages.update([...currentMessages, tempMessage]);
try {
// 3. Enviar al servidor
const sentMessage = await this.messageService.sendMessage(content);
// 4. Actualizar con la respuesta del servidor
const messagesWithSent = currentMessages.concat(sentMessage);
this.messages.update(messagesWithSent);
} catch (error) {
// 5. En caso de error, recargar para sincronizar estado
console.error('Error enviando mensaje:', error);
this.messages.reload();
}
}
// Recargar cuando se reciban notificaciones push
onNewMessageReceived() {
this.messages.reload();
}
}
Control de estado con ambos métodos
La combinación estratégica de reload() y update() permite crear experiencias de usuario fluidas que balancean rendimiento y consistencia:
export class DataDashboardComponent {
private dataService = inject(DataService);
private refreshInterval = signal<number>(5000);
dashboardData = resource({
request: () => ({ timestamp: Date.now() }),
loader: () => this.dataService.getDashboardData()
});
// Auto-refresh con reload()
private autoRefresh = effect(() => {
const interval = this.refreshInterval();
const timer = setInterval(() => {
if (!this.dashboardData.loading()) {
this.dashboardData.reload();
}
}, interval);
// Cleanup del efecto
return () => clearInterval(timer);
});
// Actualización manual optimista con update()
updateMetric(metricId: string, newValue: number) {
const current = this.dashboardData.value();
if (!current) return;
// Actualización optimista
const updated = {
...current,
metrics: current.metrics.map(m =>
m.id === metricId ? { ...m, value: newValue } : m
)
};
this.dashboardData.update(updated);
// Sincronizar con servidor
this.dataService.updateMetric(metricId, newValue).catch(() => {
// Revertir en caso de error
this.dashboardData.reload();
});
}
}
Estos métodos proporcionan un control granular sobre el estado del resource, permitiendo crear aplicaciones que respondan de manera inteligente a las interacciones del usuario mientras mantienen la sincronización con el servidor cuando es necesario.

Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Angular
Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender qué es resource() y cómo se diferencia de toSignal() en Angular.
- Aprender a manejar los estados loading, value y error de forma automática con resource().
- Configurar propiedades avanzadas como equal e injector para optimizar y contextualizar recursos.
- Utilizar los métodos reload() y update() para controlar el ciclo de vida y actualización de datos asíncronos.
- Aplicar patrones combinados para crear interfaces reactivas y robustas con manejo eficiente de datos asíncronos.