Skip to content

Dificultad de la gesti�n de estado

En RxJS, requisitos como "compartir estado entre m�ltiples componentes" o "cachear resultados de API" son muy comunes, pero elegir el m�todo adecuado es dif�cil. Esta p�gina explica patrones pr�cticos para la gesti�n de estado y compartici�n de streams.

Subject vs BehaviorSubject vs ReplaySubject

Tipos y caracter�sticas de Subject

SubjectValor inicialComportamiento al suscribirseCasos de uso comunes
SubjectNingunoSolo recibe valores posteriores a la suscripci�nEvent bus, sistema de notificaciones
BehaviorSubjectRequeridoRecibe el �ltimo valor inmediatamenteEstado actual (estado de login, item seleccionado)
ReplaySubjectNingunoRecibe los �ltimos N valoresHistorial, logs, registro de operaciones
AsyncSubjectNingunoSolo recibe el valor final al completarseResultado as�ncrono �nico (raramente usado)

Visualizaci�n de las diferencias de comportamiento de cada Subject

El siguiente diagrama muestra qu� valores recibe cada Subject al suscribirse.

Criterios de selecci�n

  • Subject: Notificaci�n de eventos (no se necesita el pasado)
  • BehaviorSubject: Gesti�n de estado (se necesita el valor actual)
  • ReplaySubject: Gesti�n de historial (se necesitan los �ltimos N valores)

Ejemplo pr�ctico 1: Subject (Event bus)

L Mal ejemplo: No se pueden recibir valores anteriores a la suscripci�n

typescript
import { Subject } from 'rxjs';

const notifications$ = new Subject<string>();

notifications$.next('Notificaci�n 1'); // Nadie est� suscrito a�n

notifications$.subscribe(msg => {
  console.log('Recibido:', msg);
});

notifications$.next('Notificaci�n 2');
notifications$.next('Notificaci�n 3');

// Salida:
// Recibido: Notificaci�n 2
// Recibido: Notificaci�n 3
// ('Notificaci�n 1' no se recibe)

 Buen ejemplo: Usar como event bus (solo procesar eventos despu�s de la suscripci�n)

typescript
import { filter, map, Subject } from 'rxjs';

class EventBus {
  private events$ = new Subject<{ type: string; payload: any }>();

  emit(type: string, payload: any) {
    this.events$.next({ type, payload });
  }

  on(type: string) {
    return this.events$.pipe(
      filter(event => event.type === type),
      map(event => event.payload)
    );
  }
}

const bus = new EventBus();

// Iniciar suscripci�n
bus.on('userLogin').subscribe(user => {
  console.log('Login:', user);
});

// Emitir evento
bus.emit('userLogin', { id: 1, name: 'Alice' }); //  Se recibe
// Login: {id: 1, name: 'Alice'}

Cu�ndo usar Subject

  • Arquitectura basada en eventos: Comunicaci�n desacoplada entre componentes
  • Sistema de notificaciones: Entrega de notificaciones en tiempo real
  • Cuando no se necesitan valores pasados: Solo procesar eventos despu�s de la suscripci�n

Ejemplo pr�ctico 2: BehaviorSubject (Gesti�n de estado)

L Mal ejemplo: Con Subject no se conoce el estado actual

typescript
import { Subject } from 'rxjs';

const isLoggedIn$ = new Subject<boolean>();

// Usuario inicia sesi�n
isLoggedIn$.next(true);

// Componente suscrito posteriormente
isLoggedIn$.subscribe(status => {
  console.log('Estado de login:', status); // No se imprime nada
});

 Buen ejemplo: Obtener estado actual inmediatamente con BehaviorSubject

typescript
import { BehaviorSubject } from 'rxjs';

class AuthService {
  private isLoggedIn$ = new BehaviorSubject<boolean>(false); // Valor inicial: false

  login(username: string, password: string) {
    // Proceso de login...
    this.isLoggedIn$.next(true);
  }

  logout() {
    this.isLoggedIn$.next(false);
  }

  // Exponer como solo lectura externamente
  get isLoggedIn() {
    return this.isLoggedIn$.asObservable();
  }

  // Obtener valor actual sincr�nicamente (solo en casos especiales)
  get currentStatus(): boolean {
    return this.isLoggedIn$.value;
  }
}

const auth = new AuthService();

auth.login('user', 'pass');

// Aunque se suscriba despu�s, puede obtener el estado actual (true) inmediatamente
auth.isLoggedIn.subscribe(status => {
  console.log('Estado de login:', status); // Estado de login: true
});

Cu�ndo usar BehaviorSubject

  • Mantener estado actual: Estado de login, item seleccionado, valores de configuraci�n
  • Necesitar valor inmediatamente al suscribirse: Cuando se necesita el estado actual para la visualizaci�n inicial de UI
  • Monitorear cambios de estado: Actualizar reactivamente cuando cambia el estado

Ejemplo pr�ctico 3: ReplaySubject (Gesti�n de historial)

 Buen ejemplo: Reproducir los �ltimos N valores

typescript
import { ReplaySubject } from 'rxjs';

class SearchHistoryService {
  // Mantener las �ltimas 5 b�squedas
  private history$ = new ReplaySubject<string>(5);

  addSearch(query: string) {
    this.history$.next(query);
  }

  getHistory() {
    return this.history$.asObservable();
  }
}

const searchHistory = new SearchHistoryService();

// Ejecutar b�squedas
searchHistory.addSearch('TypeScript');
searchHistory.addSearch('RxJS');
searchHistory.addSearch('Angular');

// Aunque se suscriba despu�s, puede obtener las �ltimas 3 b�squedas inmediatamente
searchHistory.getHistory().subscribe(query => {
  console.log('Historial de b�squeda:', query);
});

// Salida:
// Historial de b�squeda: TypeScript
// Historial de b�squeda: RxJS
// Historial de b�squeda: Angular

Cu�ndo usar ReplaySubject

  • Historial de operaciones: Historial de b�squeda, edici�n, navegaci�n
  • Logs y trazas de auditor�a: Registrar operaciones pasadas
  • Soporte para suscripci�n tard�a: Cuando se quiere recibir valores pasados aunque la suscripci�n se retrase

Diferencias entre share y shareReplay

Problema: Ejecuci�n duplicada de Observable Cold

L Mal ejemplo: API llamada m�ltiples veces con m�ltiples suscripciones

typescript
import { ajax } from 'rxjs/ajax';

const users$ = ajax.getJSON('/api/users');

// Suscripci�n 1
users$.subscribe(users => {
  console.log('Componente A:', users);
});

// Suscripci�n 2
users$.subscribe(users => {
  console.log('Componente B:', users);
});

// Problema: API llamada 2 veces
// GET /api/users (1� vez)
// GET /api/users (2� vez)

 Buen ejemplo: Convertir a Hot con share (compartir ejecuci�n)

typescript
import { ajax } from 'rxjs/ajax';
import { share } from 'rxjs';

const users$ = ajax.getJSON('/api/users').pipe(
  share() // Compartir ejecuci�n
);

// Suscripci�n 1
users$.subscribe(users => {
  console.log('Componente A:', users);
});

// Suscripci�n 2 (si se suscribe inmediatamente)
users$.subscribe(users => {
  console.log('Componente B:', users);
});

//  API llamada solo 1 vez
// GET /api/users (solo una vez)

Trampa de share

share() resetea el stream cuando se cancela la �ltima suscripci�n. Se ejecutar� de nuevo la pr�xima vez que se suscriba.

typescript
const data$ = fetchData().pipe(share());

// Suscripci�n 1
const sub1 = data$.subscribe();

// Suscripci�n 2
const sub2 = data$.subscribe();

sub1.unsubscribe();
sub2.unsubscribe(); // Todos cancelados � Reset

// Re-suscripci�n � fetchData() se ejecuta de nuevo
data$.subscribe();

shareReplay: Cachear y reutilizar resultados

 Buen ejemplo: Cachear con shareReplay

typescript
import { ajax } from 'rxjs/ajax';
import { shareReplay } from 'rxjs';

const users$ = ajax.getJSON('/api/users').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // bufferSize: 1 � Cachear el �ltimo valor
  // refCount: true � Limpiar cach� cuando se cancelen todas las suscripciones
);

// Suscripci�n 1
users$.subscribe(users => {
  console.log('Componente A:', users);
});

// Suscripci�n 2 despu�s de 1 segundo (aunque se suscriba tarde, obtiene desde cach�)
setTimeout(() => {
  users$.subscribe(users => {
    console.log('Componente B:', users); // Obtiene inmediatamente desde cach�
  });
}, 1000);

//  API llamada solo 1 vez, resultado cacheado

Comparaci�n entre share y shareReplay

Caracter�sticashare()shareReplay(1)
Nueva suscripci�n durante suscripciones activasComparte el mismo streamComparte el mismo stream
Suscripci�n tard�aSolo recibe nuevos valoresRecibe el �ltimo valor cacheado
Despu�s de cancelar todas las suscripcionesReset del streamMantiene cach� (si refCount: false)
MemoriaNo mantieneMantiene cach�
Caso de usoCompartir datos en tiempo realCachear resultados de API

 Buen ejemplo: Configuraci�n apropiada de shareReplay

typescript
import { shareReplay } from 'rxjs';

// Patr�n 1: Cach� persistente (no recomendado)
const data1$ = fetchData().pipe(
  shareReplay({ bufferSize: 1, refCount: false })
  // refCount: false � Cuidado con memory leaks
);

// Patr�n 2: Cach� con limpieza autom�tica (recomendado)
const data2$ = fetchData().pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // refCount: true � Limpiar cach� cuando se cancelen todas las suscripciones
);

// Patr�n 3: Cach� con TTL (RxJS 7.4+)
const data3$ = fetchData().pipe(
  shareReplay({
    bufferSize: 1,
    refCount: true,
    windowTime: 5000 // Descartar cach� despu�s de 5 segundos
  })
);

Advertencia sobre memory leaks

Usar shareReplay({ refCount: false }) deja la cach� persistente, causando memory leaks. B�sicamente use refCount: true.

Uso pr�ctico de Hot vs Cold

Caracter�sticas de Cold: Ejecuci�n por cada suscripci�n

typescript
import { Observable } from 'rxjs';

const cold$ = new Observable<number>(subscriber => {
  console.log('=5 Inicio de ejecuci�n');
  subscriber.next(Math.random());
  subscriber.complete();
});

cold$.subscribe(v => console.log('Suscripci�n 1:', v));
cold$.subscribe(v => console.log('Suscripci�n 2:', v));

// Salida:
// =5 Inicio de ejecuci�n
// Suscripci�n 1: 0.123
// =5 Inicio de ejecuci�n
// Suscripci�n 2: 0.456
// (Se ejecuta 2 veces, valores diferentes)

Caracter�sticas de Hot: Ejecuci�n compartida

typescript
import { Subject } from 'rxjs';

const hot$ = new Subject<number>();

hot$.subscribe(v => console.log('Suscripci�n 1:', v));
hot$.subscribe(v => console.log('Suscripci�n 2:', v));

hot$.next(Math.random());

// Salida:
// Suscripci�n 1: 0.789
// Suscripci�n 2: 0.789
// (Mismo valor compartido)

Criterios de selecci�n

RequisitoColdHot
Necesitar ejecuci�n independienteL
Compartir ejecuci�nL
Valores diferentes por suscriptorL
Entrega de datos en tiempo realL
Compartir llamadas APIL (convertir con share)

 Buen ejemplo: Conversi�n apropiada

typescript
import { interval, fromEvent } from 'rxjs';
import { share, shareReplay } from 'rxjs';

// Cold: Cada suscriptor tiene temporizador independiente
const coldTimer$ = interval(1000);

// Cold�Hot: Compartir temporizador
const hotTimer$ = interval(1000).pipe(share());

// Cold: Eventos de clic (registro de listener independiente por suscripci�n)
const clicks$ = fromEvent(document, 'click');

// Cold�Hot: Cachear resultados de API
const cachedData$ = ajax.getJSON('/api/data').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

Patr�n de gesti�n centralizada de estado

Patr�n 1: Gesti�n de estado con clase Service

typescript
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs';

interface User {
  id: number;
  name: string;
  email: string;
}

class UserStore {
  // BehaviorSubject privado
  private users$ = new BehaviorSubject<User[]>([]);

  // Observable p�blico de solo lectura
  get users(): Observable<User[]> {
    return this.users$.asObservable();
  }

  // Obtener usuario espec�fico
  getUser(id: number): Observable<User | undefined> {
    return this.users.pipe(
      map(users => users.find(u => u.id === id))
    );
  }

  // Actualizar estado
  addUser(user: User) {
    const currentUsers = this.users$.value;
    this.users$.next([...currentUsers, user]);
  }

  updateUser(id: number, updates: Partial<User>) {
    const currentUsers = this.users$.value;
    const updatedUsers = currentUsers.map(u =>
      u.id === id ? { ...u, ...updates } : u
    );
    this.users$.next(updatedUsers);
  }

  removeUser(id: number) {
    const currentUsers = this.users$.value;
    this.users$.next(currentUsers.filter(u => u.id !== id));
  }
}

// Uso
const store = new UserStore();

// Suscripci�n
store.users.subscribe(users => {
  console.log('Lista de usuarios:', users);
});

// Actualizaci�n de estado
store.addUser({ id: 1, name: 'Alice', email: 'alice@example.com' });
store.updateUser(1, { name: 'Alice Smith' });

Patr�n 2: Gesti�n de estado con Scan

typescript
import { Subject } from 'rxjs';
import { scan, startWith } from 'rxjs';

interface State {
  count: number;
  items: string[];
}

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'ADD_ITEM'; payload: string }
  | { type: 'RESET' };

const actions$ = new Subject<Action>();

const initialState: State = {
  count: 0,
  items: []
};

const state$ = actions$.pipe(
  scan((state, action) => {
    switch (action.type) {
      case 'INCREMENT':
        return { ...state, count: state.count + 1 };
      case 'DECREMENT':
        return { ...state, count: state.count - 1 };
      case 'ADD_ITEM':
        return { ...state, items: [...state.items, action.payload] };
      case 'RESET':
        return initialState;
      default:
        return state;
    }
  }, initialState),
  startWith(initialState)
);

// Suscripci�n
state$.subscribe(state => {
  console.log('Estado actual:', state);
});

// Emitir acciones
actions$.next({ type: 'INCREMENT' });
actions$.next({ type: 'ADD_ITEM', payload: 'manzana' });
actions$.next({ type: 'INCREMENT' });

// Salida:
// Estado actual: { count: 0, items: [] }
// Estado actual: { count: 1, items: [] }
// Estado actual: { count: 1, items: ['manzana'] }
// Estado actual: { count: 2, items: ['manzana'] }

Trampas comunes

Trampa 1: Exponer Subject externamente

L Mal ejemplo: Exponer Subject directamente

typescript
import { BehaviorSubject } from 'rxjs';

class BadService {
  // L Se puede modificar directamente desde fuera
  public state$ = new BehaviorSubject<number>(0);
}

const service = new BadService();

// Se puede modificar desde fuera
service.state$.next(999); // L Encapsulaci�n rota

 Buen ejemplo: Proteger con asObservable()

typescript
import { BehaviorSubject } from 'rxjs';

class GoodService {
  private _state$ = new BehaviorSubject<number>(0);

  // Exponer como solo lectura
  get state() {
    return this._state$.asObservable();
  }

  // Solo modificable a trav�s de m�todos dedicados
  increment() {
    this._state$.next(this._state$.value + 1);
  }

  decrement() {
    this._state$.next(this._state$.value - 1);
  }
}

const service = new GoodService();

//  Solo lectura posible
service.state.subscribe(value => console.log(value));

//  Modificaci�n a trav�s de m�todos dedicados
service.increment();

// L No se puede modificar directamente (error de compilaci�n)
// service.state.next(999); // Error: Property 'next' does not exist

Trampa 2: Memory leak con shareReplay

L Mal ejemplo: Memory leak con refCount: false

typescript
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';

const data$ = interval(1000).pipe(
  take(100),
  shareReplay({ bufferSize: 1, refCount: false })
  // L refCount: false � Cach� permanece para siempre
);

// Aunque se cancele la suscripci�n, el stream contin�a internamente
const sub = data$.subscribe();
sub.unsubscribe();

// Cach� permanece � Memory leak

 Buen ejemplo: Limpieza autom�tica con refCount: true

typescript
import { interval } from 'rxjs';
import { shareReplay, take } from 'rxjs';

const data$ = interval(1000).pipe(
  take(100),
  shareReplay({ bufferSize: 1, refCount: true })
  //  refCount: true � Limpieza autom�tica al cancelar todas las suscripciones
);

const sub1 = data$.subscribe();
const sub2 = data$.subscribe();

sub1.unsubscribe();
sub2.unsubscribe(); // Todas las suscripciones canceladas � Stream detenido, cach� limpiado

Trampa 3: Obtenci�n s�ncrona de valores

L Mal ejemplo: Depender demasiado de value

typescript
import { BehaviorSubject } from 'rxjs';

class CounterService {
  private count$ = new BehaviorSubject(0);

  increment() {
    // L Depender demasiado de value
    const current = this.count$.value;
    this.count$.next(current + 1);
  }

  // L Exponer obtenci�n s�ncrona
  getCurrentCount(): number {
    return this.count$.value;
  }
}

 Buen ejemplo: Mantener reactivo

typescript
import { BehaviorSubject } from 'rxjs';
import { map } from 'rxjs';

class CounterService {
  private count$ = new BehaviorSubject(0);

  get count() {
    return this.count$.asObservable();
  }

  increment() {
    //  Usar value internamente est� bien
    this.count$.next(this.count$.value + 1);
  }

  //  Devolver Observable
  isPositive() {
    return this.count$.pipe(
      map(count => count > 0)
    );
  }
}

Lista de verificaci�n de comprensi�n

Verifique si puede responder las siguientes preguntas.

markdown
## Comprensi�n b�sica
- [ ] Explicar las diferencias entre Subject, BehaviorSubject y ReplaySubject
- [ ] Entender por qu� BehaviorSubject requiere un valor inicial
- [ ] Entender el significado del bufferSize de ReplaySubject

## Hot/Cold
- [ ] Explicar las diferencias entre Observable Cold y Hot
- [ ] Explicar las diferencias entre share y shareReplay
- [ ] Entender el rol de la opci�n refCount de shareReplay

## Gesti�n de estado
- [ ] Proteger Subject sin exponerlo externamente usando asObservable()
- [ ] Implementar patr�n de gesti�n de estado con BehaviorSubject
- [ ] Entender el patr�n de gesti�n de estado con scan

## Gesti�n de memoria
- [ ] Saber c�mo prevenir memory leaks de shareReplay
- [ ] Explicar las diferencias entre refCount: true y false
- [ ] Limpiar cach� en el momento apropiado

Siguientes pasos

Despu�s de entender la gesti�n y compartici�n de estado, aprenda sobre combinaci�n de m�ltiples streams.

Combinaci�n de m�ltiples streams - Diferencias entre combineLatest, zip y withLatestFrom

P�ginas relacionadas

<� Ejercicios pr�cticos

Problema 1: Selecci�n apropiada de Subject

Elija el Subject m�s adecuado para los siguientes escenarios.

  1. Gestionar estado de login de usuario (Estado inicial: desconectado)
  2. Entrega de mensajes de notificaci�n (Solo mostrar mensajes despu�s de la suscripci�n)
  3. Mantener las �ltimas 5 operaciones del historial (Ver las �ltimas 5 aunque se suscriba tarde)
Ejemplo de respuesta

1. Estado de login de usuario

typescript
import { BehaviorSubject } from 'rxjs';

class AuthService {
  private isLoggedIn$ = new BehaviorSubject<boolean>(false);

  get loginStatus() {
    return this.isLoggedIn$.asObservable();
  }

  login() {
    this.isLoggedIn$.next(true);
  }

  logout() {
    this.isLoggedIn$.next(false);
  }
}

Raz�n

Como se necesita el estado actual inmediatamente al suscribirse, BehaviorSubject es �ptimo.


2. Entrega de mensajes de notificaci�n

typescript
import { Subject } from 'rxjs';

class NotificationService {
  private notifications$ = new Subject<string>();

  get messages() {
    return this.notifications$.asObservable();
  }

  notify(message: string) {
    this.notifications$.next(message);
  }
}

Raz�n

Como solo se necesitan mostrar mensajes despu�s de la suscripci�n, Subject es suficiente.


3. �ltimas 5 operaciones del historial

typescript
import { ReplaySubject } from 'rxjs';

class HistoryService {
  private actions$ = new ReplaySubject<string>(5); // Mantener 5

  get history() {
    return this.actions$.asObservable();
  }

  addAction(action: string) {
    this.actions$.next(action);
  }
}

Raz�n

Para mantener las �ltimas 5 y poder obtenerlas aunque se suscriba tarde, ReplaySubject(5) es �ptimo.

Problema 2: Selecci�n de share o shareReplay

Elija el operador apropiado para los siguientes c�digos.

typescript
import { ajax } from 'rxjs/ajax';

// Escenario 1: Datos en tiempo real desde WebSocket
const realTimeData$ = webSocket('ws://example.com/stream');

// Escenario 2: Llamada API de informaci�n de usuario (quiere cachear resultado)
const user$ = ajax.getJSON('/api/user/me');

// �Qu� usar para cada uno?
Ejemplo de respuesta

Escenario 1: Datos en tiempo real desde WebSocket

typescript
import { share } from 'rxjs';

const realTimeData$ = webSocket('ws://example.com/stream').pipe(
  share() // Datos en tiempo real no necesitan cach�
);

Raz�n

Los datos en tiempo real como WebSocket no necesitan cachear valores pasados, por lo que se usa share(). Si se suscribe tarde, recibir� nuevos datos desde ese punto.


Escenario 2: Llamada API de informaci�n de usuario

typescript
import { shareReplay } from 'rxjs';

const user$ = ajax.getJSON('/api/user/me').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);

Raz�n

Como se quiere cachear el resultado de la API y compartirlo entre m�ltiples componentes, se usa shareReplay(). refCount: true previene memory leaks.

Problema 3: Correcci�n de memory leak

El siguiente c�digo tiene un problema de memory leak. Corr�jalo.

typescript
import { interval } from 'rxjs';
import { shareReplay } from 'rxjs';

const data$ = interval(1000).pipe(
  shareReplay(1) // Problema: esto es igual a shareReplay({ bufferSize: 1, refCount: false })
);

const sub = data$.subscribe(v => console.log(v));
sub.unsubscribe();

// Despu�s de esto, interval sigue ejecut�ndose � Memory leak
Ejemplo de respuesta

C�digo corregido:

typescript
import { interval } from 'rxjs';
import { shareReplay } from 'rxjs';

const data$ = interval(1000).pipe(
  shareReplay({ bufferSize: 1, refCount: true })
  // refCount: true � Stream se detiene al cancelar todas las suscripciones
);

const sub = data$.subscribe(v => console.log(v));
sub.unsubscribe(); // Stream se detiene

Problema

  • shareReplay(1) es la forma abreviada de shareReplay({ bufferSize: 1, refCount: false })
  • Con refCount: false, el stream contin�a ejecut�ndose despu�s de cancelar todas las suscripciones
  • interval sigue ejecut�ndose eternamente, causando memory leak

Raz�n de la correcci�n

Especificando refCount: true, el stream tambi�n se detiene cuando se cancela la �ltima suscripci�n, y se limpia la cach�.

Problema 4: Implementaci�n de gesti�n de estado

Implemente un TodoStore que cumpla los siguientes requisitos.

Requisitos

  • Poder agregar, completar y eliminar items Todo
  • Obtener lista de Todos como solo lectura externamente
  • Obtener n�mero de Todos completados
Ejemplo de respuesta
typescript
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

class TodoStore {
  private todos$ = new BehaviorSubject<Todo[]>([]);
  private nextId = 1;

  // Exponer como solo lectura
  get todos(): Observable<Todo[]> {
    return this.todos$.asObservable();
  }

  // N�mero de Todos completados
  get completedCount(): Observable<number> {
    return this.todos$.pipe(
      map(todos => todos.filter(t => t.completed).length)
    );
  }

  // Agregar Todo
  addTodo(text: string) {
    const currentTodos = this.todos$.value;
    const newTodo: Todo = {
      id: this.nextId++,
      text,
      completed: false
    };
    this.todos$.next([...currentTodos, newTodo]);
  }

  // Completar Todo
  toggleTodo(id: number) {
    const currentTodos = this.todos$.value;
    const updatedTodos = currentTodos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    this.todos$.next(updatedTodos);
  }

  // Eliminar Todo
  removeTodo(id: number) {
    const currentTodos = this.todos$.value;
    this.todos$.next(currentTodos.filter(todo => todo.id !== id));
  }
}

// Uso
const store = new TodoStore();

store.todos.subscribe(todos => {
  console.log('Lista de Todos:', todos);
});

store.completedCount.subscribe(count => {
  console.log('Completados:', count);
});

store.addTodo('Aprender RxJS');
store.addTodo('Leer documentaci�n');
store.toggleTodo(1);

Puntos clave

  • Mantener estado con BehaviorSubject
  • Exponer externamente como solo lectura con asObservable()
  • Usar value para obtener y actualizar el estado actual
  • Usar map para calcular estado derivado (completedCount)

Publicado bajo licencia CC-BY-4.0.