InterviewHack.ai
Start free
Blog/Preguntas de entrevista de Angular — 40 con código y respuestas

Preguntas de entrevista de Angular — 40 con código y respuestas

16 de septiembre de 2026

angulartypescript

Las 40 preguntas de Angular que realmente se hacen en entrevistas técnicas: inyección de dependencias, RxJS, change detection, lazy loading, signals. Respuestas con código TypeScript.

Preguntas de entrevista de Angular — 40 con código y respuestas

Si estás preparando una entrevista técnica para un rol de Angular, sabés que no alcanza con haber usado el framework: te van a preguntar *por qué* funciona como funciona. Esta guía cubre exactamente las 40 preguntas que más se repiten en procesos de selección reales, con la respuesta completa, código TypeScript listo para copiar, y los errores que te descartan si los cometés.


Componentes y directivas

1. ¿Cuál es la diferencia entre un componente y una directiva en Angular?

Por qué la hacen: quieren ver si entendés la jerarquía conceptual del framework, no solo si sabés crear archivos con el CLI.

Un componente es una directiva con template. Internamente, @Component extiende @Directive. La distinción práctica: los componentes tienen su propio DOM (selector de elemento), las directivas modifican el DOM existente (selectores de atributo o clase).

typescript
// Directiva estructural personalizada
@Directive({ selector: '[appUnlessRole]', standalone: true })
export class UnlessRoleDirective {
  private vcr = inject(ViewContainerRef);
  private tpl = inject(TemplateRef<unknown>);

  @Input() set appUnlessRole(role: string) {
    const user = inject(AuthService).currentUser();
    if (user?.role !== role) {
      this.vcr.createEmbeddedView(this.tpl);
    } else {
      this.vcr.clear();
    }
  }
}

Error común: decir "los componentes tienen HTML y las directivas no". Técnicamente cierto, pero la explicación correcta es la de jerarquía. Si solo repetís la definición superficial, el entrevistador va a profundizar hasta que se te acabe el hilo.


2. ¿Cómo funciona el ciclo de vida de un componente? Nombrá los hooks en orden.

Por qué la hacen: los bugs de producción más frecuentes en Angular involucran mal uso de ngOnInit vs ngAfterViewInit vs ngOnDestroy.

Orden completo:

  1. 1constructor — inyección de dependencias, nada del DOM todavía
  2. 2ngOnChanges — se dispara antes de ngOnInit si hay @Input()
  3. 3ngOnInit — el componente está inicializado, los inputs tienen valor
  4. 4ngDoCheck — cada ciclo de detección de cambios
  5. 5ngAfterContentInit — después de proyectar contenido con
  6. 6ngAfterContentChecked
  7. 7ngAfterViewInit — el DOM del componente (y sus hijos) ya existe
  8. 8ngAfterViewChecked
  9. 9ngOnDestroy — antes de destruir el componente
typescript
@Component({ selector: 'app-demo', template: '' })
export class DemoComponent implements OnInit, AfterViewInit, OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit(): void {
    // ✅ Acá van las llamadas HTTP, subscripciones, init de estado
  }

  ngAfterViewInit(): void {
    // ✅ Acá podés leer @ViewChild (antes de esto son undefined)
  }

  ngOnDestroy(): void {
    // ✅ SIEMPRE limpiar subscripciones
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Error común: hacer llamadas HTTP en el constructor. Funciona, pero no tenés acceso a los @Input() todavía y el testing se complica.


3. ¿Cuándo usarías `@ViewChild` vs `@ContentChild`?

Por qué la hacen: confundirlos es señal de que armás componentes pero no los *diseñás*.

  • @ViewChild: accede a elementos o componentes dentro del propio template del componente.
  • @ContentChild: accede a contenido proyectado desde afuera con .
typescript
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <ng-content select="[slot=header]"></ng-content>
      <div #body class="body">...</div>
    </div>
  `
})
export class CardComponent implements AfterViewInit, AfterContentInit {
  @ViewChild('body') bodyEl!: ElementRef;           // dentro del template propio
  @ContentChild('headerTitle') headerTitle?: ElementRef; // proyectado desde afuera

  ngAfterViewInit() {
    console.log(this.bodyEl.nativeElement); // ✅ disponible acá
  }

  ngAfterContentInit() {
    console.log(this.headerTitle?.nativeElement); // ✅ disponible acá
  }
}

4. ¿Qué es `ng-content` y cómo usás múltiples slots?

Por qué la hacen: mide si sabés construir componentes reutilizables de verdad.

typescript
// componente de card con múltiples slots
@Component({
  selector: 'app-panel',
  template: `
    <section>
      <header><ng-content select="[slot=header]"></ng-content></header>
      <main><ng-content></ng-content></main>
      <footer><ng-content select="[slot=footer]"></ng-content></footer>
    </section>
  `
})
export class PanelComponent {}

// uso
// <app-panel>
//   <h2 slot="header">Título</h2>
//   <p>Contenido principal</p>
//   <button slot="footer">Cerrar</button>
// </app-panel>

5. ¿Cuál es la diferencia entre `*ngIf` y `[hidden]`?

Por qué la hacen: el impacto en performance es real y los entrevistadores quieren ver que lo sabés.

  • *ngIf="false": remueve el elemento del DOM completamente. Los componentes hijos se destruyen (se ejecuta ngOnDestroy).
  • [hidden]="true": agrega display: none pero el elemento sigue en el DOM. Los componentes siguen vivos.

Usá [hidden] cuando el toggle es frecuente y el componente tiene estado que querés preservar. Usá *ngIf cuando querés liberar recursos o evitar renderizar algo que casi nunca se ve.


6. ¿Qué son las directivas `standalone` y por qué se adoptaron?

Por qué la hacen: desde Angular 14 en adelante, los standalone components son el camino recomendado.

Antes de standalone, todo componente/directiva/pipe tenía que declararse en un NgModule. Eso creaba overhead cognitivo y dificultaba el tree-shaking. Con standalone:

typescript
@Component({
  selector: 'app-hero',
  standalone: true,
  imports: [CommonModule, RouterLink, HeroCardComponent],
  template: `...`
})
export class HeroComponent {}

No necesitás un módulo contenedor. El grafo de dependencias es explícito en el decorador. El bundler puede hacer tree-shaking más agresivo.


Inyección de dependencias

7. Explicá el sistema de DI de Angular. ¿Cuáles son los distintos scopes de un proveedor?

Por qué la hacen: es uno de los temas que más diferencian a un dev Angular junior de uno senior.

Angular tiene un árbol de injectors que espeja el árbol de componentes. Los scopes son:

| Scope | Cómo se configura | Cuándo se destruye |

|---|---|---|

| Root (singleton) | providedIn: 'root' | Nunca (dura toda la app) |

| Platform | providedIn: 'platform' | Cuando se destruye la plataforma |

| Module | providers: [] en NgModule | Cuando se destruye el módulo |

| Component | providers: [] en @Component | Cuando se destruye el componente |

| Environment Injector | providers en bootstrapApplication | Configurable |

typescript
// Singleton global
@Injectable({ providedIn: 'root' })
export class AuthService { }

// Instancia por componente — cada instancia del componente tiene su propio servicio
@Component({
  providers: [CartService] // nueva instancia por cada CartComponent
})
export class CartComponent { }

Error común: asumir que providedIn: 'root' y declarar en AppModule.providers son lo mismo. No: providedIn: 'root' permite tree-shaking (si nadie inyecta el servicio, no va al bundle). La declaración en el array providers del módulo siempre lo incluye.


8. ¿Qué hace `inject()` y cuándo preferirlo al constructor?

Por qué la hacen: inject() es el patrón moderno, pero hay reglas estrictas de cuándo podés usarlo.

typescript
// Patrón clásico (constructor injection)
@Injectable({ providedIn: 'root' })
export class OldService {
  constructor(private http: HttpClient, private auth: AuthService) {}
}

// Patrón moderno con inject()
@Injectable({ providedIn: 'root' })
export class ModernService {
  private http = inject(HttpClient);
  private auth = inject(AuthService);
}

inject() solo puede llamarse en un injection context: constructor de una clase, inicializador de campo de clase, o factory function. Llamarlo fuera de ese contexto lanza error en runtime.

La ventaja práctica: podés crear funciones helper reutilizables que internamente inyectan dependencias.

typescript
// Factory reutilizable
function createLogger(prefix: string) {
  const logger = inject(LoggerService);
  return (msg: string) => logger.log(`[${prefix}] ${msg}`);
}

@Component({ ... })
export class MyComponent {
  private log = createLogger('MyComponent'); // ✅ funciona porque estamos en injection context
}

9. ¿Qué son los tokens de inyección (`InjectionToken`) y cuándo los usás?

Por qué la hacen: quieren ver que sabés tipear correctamente dependencias que no son clases.

typescript
// Definición del token
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// Proveedor
export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_CONFIG,
      useValue: { apiUrl: 'https://api.example.com', version: 'v2' }
    }
  ]
};

// Consumo
@Injectable({ providedIn: 'root' })
export class ApiService {
  private config = inject(APP_CONFIG);

  getUrl(path: string): string {
    return `${this.config.apiUrl}/${this.config.version}/${path}`;
  }
}

Los InjectionToken son necesarios para inyectar valores primitivos, interfaces (que no existen en runtime) y configuración de ambiente.


10. ¿Qué es `useClass`, `useValue`, `useExisting`, `useFactory`?

Por qué la hacen: los providers avanzados aparecen en código de producción real.

typescript
providers: [
  // useClass: Angular crea una instancia de MockAuthService cuando pidan AuthService
  { provide: AuthService, useClass: MockAuthService },

  // useValue: inyecta el valor directamente (no crea instancia)
  { provide: BASE_URL, useValue: environment.apiUrl },

  // useExisting: alias — comparte la MISMA instancia ya creada
  { provide: Logger, useExisting: ConsoleLoggerService },

  // useFactory: construye el valor con lógica dinámica
  {
    provide: AnalyticsService,
    useFactory: (platform: Platform) => {
      return platform.isBrowser ? new MixpanelService() : new NoopAnalyticsService();
    },
    deps: [Platform]
  }
]

RxJS y operadores

11. ¿Cuál es la diferencia entre `switchMap`, `mergeMap`, `concatMap` y `exhaustMap`?

Por qué la hacen: es la pregunta de RxJS más frecuente en entrevistas. Si no la sabés responder bien, es señal de que no trabajaste con streams reales.

Todos transforman cada valor emitido en un Observable interno. La diferencia está en qué hacen con las subscripciones simultáneas:

| Operador | Comportamiento | Caso de uso típico |

|---|---|---|

| switchMap | Cancela el Observable anterior al llegar uno nuevo | Búsqueda en tiempo real (typeahead) |

| mergeMap | Mantiene todos los Observables activos en paralelo | Uploads múltiples simultáneos |

| concatMap | Cola — espera a que termine el anterior | Animaciones secuenciales, operaciones ordenadas |

| exhaustMap | Ignora nuevos valores mientras hay uno activo | Login button (evitar doble submit) |

typescript
// switchMap — búsqueda: si el usuario escribe rápido, cancela la búsqueda anterior
this.searchControl.valueChanges.pipe(
  debounceTime(300),
  switchMap(query => this.api.search(query)) // cancela HTTP anterior ✅
).subscribe(results => this.results = results);

// exhaustMap — botón de pago: ignora clicks mientras procesa
fromEvent(this.payBtn.nativeElement, 'click').pipe(
  exhaustMap(() => this.paymentService.charge()) // ignora clicks extra ✅
).subscribe();

// concatMap — subir archivos en orden
from(this.files).pipe(
  concatMap(file => this.upload(file)) // uno por uno, en orden ✅
).subscribe();

Error común: usar mergeMap para typeahead. Si el usuario escribe "an" y luego "angular", pueden llegar las respuestas en orden invertido y mostrar resultados incorrectos.


12. ¿Qué hace `combineLatest` y cuándo lo usás?

Por qué la hacen: es la forma idiomática de combinar múltiples streams de estado.

combineLatest([a$, b$]) emite cada vez que *cualquiera* de los observables emite, usando el último valor conocido de los demás. Solo emite cuando todos han emitido al menos una vez.

typescript
// Formulario reactivo que combina filtros
const results$ = combineLatest([
  this.searchQuery$,
  this.categoryFilter$,
  this.priceRange$
]).pipe(
  debounceTime(200),
  switchMap(([query, category, price]) =>
    this.productService.search({ query, category, price })
  )
);

Diferencia con forkJoin: forkJoin espera a que *todos* completen (como Promise.all). combineLatest sigue emitiendo mientras los streams estén vivos. Usá forkJoin para HTTP requests paralelos que necesitás combinar una sola vez.


13. ¿Cómo evitás memory leaks con RxJS en Angular?

Por qué la hacen: los leaks de subscripciones son el problema #1 de performance en apps Angular.

Hay tres patrones aceptados:

typescript
// Patrón 1: takeUntilDestroyed (Angular 16+, preferido)
@Component({ ... })
export class MyComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.data$.pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(data => this.data = data);
  }
}

// Patrón 2: async pipe (el más limpio, maneja todo solo)
// template: {{ data$ | async }}

// Patrón 3: Subject + takeUntil (pre-Angular 16)
@Component({ ... })
export class OldComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    this.data$.pipe(takeUntil(this.destroy$)).subscribe(...);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

14. ¿Cuál es la diferencia entre `Subject`, `BehaviorSubject`, `ReplaySubject` y `AsyncSubject`?

Por qué la hacen: elegir el tipo incorrecto causa bugs de race condition difíciles de reproducir.

typescript
// Subject: no tiene valor inicial, solo emite hacia adelante
const events$ = new Subject<string>();

// BehaviorSubject: tiene valor inicial, siempre emite el último a nuevos suscriptores
const currentUser$ = new BehaviorSubject<User | null>(null);
currentUser$.next(loggedInUser);
currentUser$.value; // acceso sincrónico al valor actual

// ReplaySubject(n): emite los últimos n valores a nuevos suscriptores
const recentActions$ = new ReplaySubject<Action>(5);

// AsyncSubject: solo emite el último valor cuando el Observable completa
const httpResult$ = new AsyncSubject<Response>();

Cuándo usar cada uno:

  • BehaviorSubject → estado compartido (usuario actual, tema, idioma)
  • ReplaySubject → caché de eventos recientes, websockets donde nuevos suscriptores necesitan contexto
  • Subject → eventos fire-and-forget sin estado

15. ¿Qué diferencia hay entre `Observable` frío y caliente?

Por qué la hacen: entender esto es fundamental para no duplicar llamadas HTTP.

  • Frío (cold): la lógica del producer empieza para cada suscriptor. Los Observables de HttpClient son fríos — cada subscribe() hace un HTTP request nuevo.
  • Caliente (hot): el producer existe independientemente. Los subjects y fromEvent son calientes.
typescript
// Problema: dos subscripciones = dos HTTP requests
const data$ = this.http.get('/api/data');
data$.subscribe(a => ...); // request 1
data$.subscribe(b => ...); // request 2

// Solución: compartir el stream
const sharedData$ = this.http.get('/api/data').pipe(shareReplay(1));
sharedData$.subscribe(a => ...); // request 1
sharedData$.subscribe(b => ...); // reutiliza el resultado cacheado ✅

Change Detection

16. ¿Cómo funciona el sistema de Change Detection de Angular?

Por qué la hacen: es lo que separa a los devs que saben optimizar de los que no.

Angular tiene un árbol de vistas. Por default, después de cada evento asíncrono (click, HTTP, timer), Zone.js notifica a Angular, que recorre el árbol de arriba a abajo y verifica si algo cambió para actualizar el DOM.

Default: verifica todos los componentes del árbol en cada ciclo.

OnPush: solo verifica el componente cuando:

  1. 1Un @Input() recibe una nueva referencia de objeto
  2. 2Un evento ocurre dentro del componente
  3. 3Un Observable al que el template está suscripto con async emite
  4. 4Se llama markForCheck() manualmente

17. ¿Cuándo usás `ChangeDetectionStrategy.OnPush` y qué implicaciones tiene?

Por qué la hacen: es la optimización de performance más impactante en Angular y muchos devs la conocen pero no entienden sus implicaciones.

typescript
@Component({
  selector: 'app-product-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>{{ product.name }}</div>
    <span>{{ product.price | currency }}</span>
  `
})
export class ProductCardComponent {
  @Input() product!: Product; // ✅ OnPush detecta cambios si llega un nuevo objeto
}

Implicación crítica: mutaciones de objeto no se detectan.

typescript
// ❌ No dispara change detection con OnPush
this.product.price = 99;

// ✅ Sí dispara — nueva referencia
this.product = { ...this.product, price: 99 };

Si tenés que actualizar sin cambiar referencia, usás ChangeDetectorRef.markForCheck():

typescript
constructor(private cdr: ChangeDetectorRef) {}

updatePrice() {
  this.product.price = 99; // mutación
  this.cdr.markForCheck(); // forzar re-render
}

18. ¿Qué es `markForCheck()` vs `detectChanges()`?

Ambos viven en ChangeDetectorRef pero tienen comportamientos distintos:

  • markForCheck(): marca el componente y sus ancestros para ser verificados en el *próximo* ciclo de change detection. No es sincrónico.
  • detectChanges(): dispara change detection inmediatamente para este componente y sus hijos. Sincrónico. Útil en contextos fuera de Zone.js.
typescript
// Cuando necesitás actualización inmediata (ej: integración con librería externa)
this.cdr.detectChanges(); // sincrónico

// Cuando estás dentro del ciclo de Angular y solo necesitás marcar para el próximo tick
this.cdr.markForCheck(); // asincrónico, más performante

19. ¿Qué es Zone.js y cómo afecta a Angular?

Por qué la hacen: con Zoneless Angular (experimental desde v16, estable en v18), es un tema cada vez más relevante.

Zone.js es una librería que parchea las APIs asíncronas del browser (setTimeout, Promise, fetch, addEventListener) para que Angular sepa cuándo terminó una operación asíncrona y tiene que verificar cambios.

Problema: parchea TODO, incluyendo librerías de terceros que no tienen nada que ver con tu app. Eso causa ciclos de change detection innecesarios.

Zoneless Angular (v18+):

typescript
// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection() // sin Zone.js
  ]
});

Con zoneless, Angular solo actualiza cuando los signals cambian o cuando se llama a markForCheck()/detectChanges() explícitamente.


Lazy Loading

20. ¿Cómo implementás lazy loading de rutas en Angular?

Por qué la hacen: es una pregunta de performance casi obligatoria en cualquier entrevista.

typescript
// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./dashboard/dashboard.component').then(m => m.DashboardComponent)
  },
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes').then(m => m.adminRoutes)
  }
];

Con standalone components podés usar loadComponent directamente. Con módulos usás loadChildren.

Por qué importa: el bundle inicial de la app solo incluye lo que se necesita para la primera pantalla. El resto se descarga on-demand, reduciendo el First Contentful Paint.


21. ¿Qué es `preloadingStrategy` y cuáles son las opciones?

Por qué la hacen: lazy loading puro puede introducir latencia perceptible al navegar. Las estrategias de preload equilibran eso.

typescript
import { PreloadAllModules, withPreloading } from '@angular/router';

// Opciones:
// NoPreloading (default): no prefetch nada
// PreloadAllModules: prefetch todo después del load inicial
// QuicklinkStrategy: prefetch solo las rutas visibles en pantalla (requiere ngx-quicklink)

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes, withPreloading(PreloadAllModules))
  ]
});

Estrategia personalizada:

typescript
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    return route.data?.['preload'] ? load() : EMPTY;
  }
}

// En rutas
{ path: 'reports', loadChildren: ..., data: { preload: true } }

22. ¿Cómo analizás el tamaño de tu bundle en Angular?

bash
# Generar stats
ng build --stats-json

# Analizar con webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/my-app/stats.json

También podés usar ng build con --source-map y la extensión Source Map Explorer. Los candidatos que saben esto demuestran que se preocupan por la experiencia del usuario en producción.


Angular Signals (v17+)

23. ¿Qué son los Signals y qué problema resuelven?

Por qué la hacen: signals son el cambio arquitectural más grande de Angular desde la introducción de Ivy.

Un Signal es un wrapper reactivo alrededor de un valor que notifica a Angular *exactamente* qué cambió, sin necesidad de Zone.js ni de recorrer todo el árbol de componentes.

typescript
import { signal, computed, effect } from '@angular/core';

@Component({ ... })
export class CounterComponent {
  // Signal writable
  count = signal(0);

  // Signal derivado (computed) — se actualiza automáticamente
  doubled = computed(() => this.count() * 2);

  increment() {
    this.count.update(v => v + 1); // o this.count.set(this.count() + 1)
  }
}
html
<!-- En el template, los signals se llaman como funciones -->
<p>{{ count() }}</p>
<p>{{ doubled() }}</p>

Ventaja vs BehaviorSubject: no necesitás desuscribirte, no hay | async, el template es más claro, y Angular puede hacer change detection fino (granular).


24. ¿Qué hace `effect()` en Angular Signals?

effect() ejecuta un callback reactivo cada vez que algún signal que lee adentro cambia. Se ejecuta al menos una vez al inicializarse.

typescript
@Component({ ... })
export class ThemeComponent {
  theme = signal<'light' | 'dark'>('light');

  constructor() {
    effect(() => {
      // Cada vez que theme() cambie, este código se ejecuta
      document.body.setAttribute('data-theme', this.theme());
    });
  }
}

Advertencia: no modificar signals dentro de un effect() por default — crea ciclos. Si necesitás hacerlo, usá { allowSignalWrites: true }.


25. ¿Cuál es la diferencia entre `signal()`, `computed()` y `toSignal()`?

typescript
import { signal, computed, toSignal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';

// signal(): valor reactivo mutable
const price = signal(100);

// computed(): señal de solo lectura derivada de otras señales
const withTax = computed(() => price() * 1.21);

// toSignal(): convierte un Observable en Signal (se limpia solo)
@Component({ ... })
export class ProductComponent {
  private productService = inject(ProductService);

  // Convierte Observable en Signal — no necesita async pipe ni unsubscribe
  products = toSignal(this.productService.getAll(), { initialValue: [] });
}
html
<!-- Sin async pipe -->
@for (p of products(); track p.id) {
  <app-product-card [product]="p" />
}

26. ¿Cómo migrás de BehaviorSubject a Signals?

Por qué la hacen: las migraciones de código existente son escenarios reales que enfrentan los equipos.

typescript
// Antes (BehaviorSubject)
@Injectable({ providedIn: 'root' })
export class CartService {
  private _items$ = new BehaviorSubject<CartItem[]>([]);
  items$ = this._items$.asObservable();
  totalItems$ = this._items$.pipe(map(items => items.length));

  addItem(item: CartItem) {
    this._items$.next([...this._items$.getValue(), item]);
  }
}

// Después (Signals)
@Injectable({ providedIn: 'root' })
export class CartService {
  items = signal<CartItem[]>([]);
  totalItems = computed(() => this.items().length);

  addItem(item: CartItem) {
    this.items.update(current => [...current, item]);
  }
}

NgRx

27. ¿Cuándo tiene sentido usar NgRx?

Por qué la hacen: muchos devs agregan NgRx en apps pequeñas donde es overkill. Los entrevistadores quieren ver criterio.

NgRx tiene sentido cuando:

  • El estado se comparte entre muchos componentes no relacionados en el árbol
  • Necesitás trazar cambios de estado (time-travel debugging, auditoría)
  • Múltiples acciones disparan la misma mutación de estado
  • El estado survives navegación entre rutas

Para estado local de componentes, usá signals. Para estado compartido simple, un servicio con signals alcanza. NgRx agrega boilerplate real.


28. Explicá el flujo Action → Reducer → Selector en NgRx.

typescript
// 1. Action
export const loadProducts = createAction('[Products] Load Products');
export const loadProductsSuccess = createAction(
  '[Products] Load Products Success',
  props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
  '[Products] Load Products Failure',
  props<{ error: string }>()
);

// 2. Reducer
interface ProductsState {
  products: Product[];
  loading: boolean;
  error: string | null;
}

const initialState: ProductsState = {
  products: [],
  loading: false,
  error: null
};

export const productsReducer = createReducer(
  initialState,
  on(loadProducts, state => ({ ...state, loading: true })),
  on(loadProductsSuccess, (state, { products }) => ({
    ...state, loading: false, products
  })),
  on(loadProductsFailure, (state, { error }) => ({
    ...state, loading: false, error
  }))
);

// 3. Selector
export const selectProducts = createSelector(
  (state: AppState) => state.products,
  (productsState) => productsState.products
);

export const selectIsLoading = createSelector(
  (state: AppState) => state.products,
  (productsState) => productsState.loading
);

29. ¿Qué son los Effects en NgRx y cómo los estructurás?

Los Effects manejan side effects (HTTP, localStorage, etc.) fuera del reducer.

typescript
@Injectable()
export class ProductEffects {
  private actions$ = inject(Actions);
  private productService = inject(ProductService);

  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadProducts),
      switchMap(() =>
        this.productService.getAll().pipe(
          map(products => loadProductsSuccess({ products })),
          catchError(error => of(loadProductsFailure({ error: error.message })))
        )
      )
    )
  );
}

Error común: no manejar el error dentro del switchMap con catchError. Si el Observable interno lanza sin captura, el effect muere y nunca vuelve a funcionar.


Guards de Routing

30. ¿Cuáles son los guards disponibles y cuándo usás cada uno?

Por qué la hacen: los guards son la primera línea de defensa en navegación.

| Guard | Cuándo se ejecuta | Retorna |

|---|---|---|

| canActivate | Antes de activar una ruta | boolean / UrlTree |

| canActivateChild | Antes de activar rutas hijas | boolean / UrlTree |

| canDeactivate | Antes de salir de una ruta | boolean (confirmar salida) |

| canMatch | Antes de cargar el módulo lazy | boolean |

| resolve | Antes de activar, carga datos | datos para la ruta |

typescript
// Guard funcional (Angular 15+, patrón recomendado)
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);

  if (auth.isLoggedIn()) {
    return true;
  }

  // Retorna UrlTree para redirigir (mejor que router.navigate())
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Uso
{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }

31. ¿Cómo implementás un guard de confirmación antes de salir (`canDeactivate`)?

typescript
// Interfaz del componente protegido
export interface CanComponentDeactivate {
  canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
}

// Guard
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> =
  (component) => {
    return component.canDeactivate?.() ?? true;
  };

// Componente
@Component({ ... })
export class EditFormComponent implements CanComponentDeactivate {
  form = inject(FormBuilder).group({ ... });

  canDeactivate(): boolean {
    if (this.form.dirty) {
      return confirm('Tenés cambios sin guardar. ¿Querés salir igual?');
    }
    return true;
  }
}

Interceptores HTTP

32. ¿Cómo implementás un interceptor HTTP en Angular?

Por qué la hacen: los interceptores son infra crítica en cualquier app real (auth tokens, manejo de errores global, loading states).

typescript
// Interceptor funcional (Angular 15+)
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.getToken();

  if (!token) return next(req);

  const authReq = req.clone({
    headers: req.headers.set('Authorization', `Bearer ${token}`)
  });

  return next(authReq);
};

// Registro
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor]))
  ]
});

33. ¿Cómo implementás retry automático y manejo de errores global?

typescript
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  const notify = inject(NotificationService);

  return next(req).pipe(
    retry({
      count: 3,
      delay: (error, retryCount) => {
        // Solo reintenta errores de red (5xx), no errores de cliente (4xx)
        if (error.status >= 400 && error.status < 500) {
          return throwError(() => error);
        }
        return timer(Math.pow(2, retryCount) * 1000); // exponential backoff
      }
    }),
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        router.navigate(['/login']);
      } else if (error.status >= 500) {
        notify.error('Error del servidor. Intentá de nuevo más tarde.');
      }
      return throwError(() => error);
    })
  );
};

34. ¿Cómo agregás un loading global con interceptores?

typescript
@Injectable({ providedIn: 'root' })
export class LoadingService {
  private pendingRequests = signal(0);
  isLoading = computed(() => this.pendingRequests() > 0);

  increment() { this.pendingRequests.update(n => n + 1); }
  decrement() { this.pendingRequests.update(n => Math.max(0, n - 1)); }
}

export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
  const loading = inject(LoadingService);
  loading.increment();

  return next(req).pipe(
    finalize(() => loading.decrement())
  );
};

Testing con TestBed

35. ¿Cómo testeás un componente con TestBed?

Por qué la hacen: los tests en Angular tienen bastante boilerplate específico que no es obvio al principio.

typescript
describe('ProductCardComponent', () => {
  let component: ProductCardComponent;
  let fixture: ComponentFixture<ProductCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ProductCardComponent], // standalone component
    }).compileComponents();

    fixture = TestBed.createComponent(ProductCardComponent);
    component = fixture.componentInstance;

    // Configurar inputs obligatorios
    component.product = { id: 1, name: 'Test', price: 100 };
    fixture.detectChanges(); // triggerea ngOnInit y primer render
  });

  it('should display product name', () => {
    const el = fixture.nativeElement.querySelector('.product-name');
    expect(el.textContent).toContain('Test');
  });

  it('should emit addToCart event on button click', () => {
    const spy = jest.spyOn(component.addToCart, 'emit');
    const btn = fixture.nativeElement.querySelector('[data-testid="add-btn"]');
    btn.click();
    expect(spy).toHaveBeenCalledWith(component.product);
  });
});

36. ¿Cómo mockiás servicios en TestBed?

typescript
describe('ProductListComponent', () => {
  let productService: jasmine.SpyObj<ProductService>;

  beforeEach(async () => {
    const spy = jasmine.createSpyObj('ProductService', ['getAll']);

    await TestBed.configureTestingModule({
      imports: [ProductListComponent],
      providers: [
        { provide: ProductService, useValue: spy }
      ]
    }).compileComponents();

    productService = TestBed.inject(ProductService) as jasmine.SpyObj<ProductService>;
    productService.getAll.and.returnValue(of([
      { id: 1, name: 'Product A', price: 50 }
    ]));
  });

  it('should load and display products', fakeAsync(() => {
    const fixture = TestBed.createComponent(ProductListComponent);
    fixture.detectChanges();
    tick(); // avanza el event loop
    fixture.detectChanges();

    const items = fixture.nativeElement.querySelectorAll('.product-item');
    expect(items.length).toBe(1);
  }));
});

37. ¿Qué es `fakeAsync` y cuándo lo usás?

fakeAsync te da control sobre el tiempo en tests. Dentro de fakeAsync, tick(ms) avanza el reloj virtual sin esperar de verdad.

typescript
it('should debounce search input', fakeAsync(() => {
  const fixture = TestBed.createComponent(SearchComponent);
  const component = fixture.componentInstance;
  fixture.detectChanges();

  const input = fixture.nativeElement.querySelector('input');
  input.value = 'angular';
  input.dispatchEvent(new Event('input'));

  // Sin tick, el debounceTime(300) no habrá pasado
  expect(component.results).toEqual([]);

  tick(300); // avanza 300ms virtuales
  fixture.detectChanges();

  expect(component.results.length).toBeGreaterThan(0);

  discardPeriodicTasks(); // limpiar timers pendientes
}));

38. ¿Cómo testeás un servicio con dependencias HTTP usando `HttpClientTestingModule`?

typescript
describe('ProductService', () => {
  let service: ProductService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ProductService,
        provideHttpClientTesting()
      ]
    });

    service = TestBed.inject(ProductService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify(); // verifica que no hay requests sin atender
  });

  it('should fetch products', () => {
    const mockProducts = [{ id: 1, name: 'Angular Guide', price: 29 }];

    service.getAll().subscribe(products => {
      expect(products).toEqual(mockProducts);
    });

    const req = httpMock.expectOne('/api/products');
    expect(req.request.method).toBe('GET');
    req.flush(mockProducts); // simula la respuesta
  });
});

39. ¿Cómo testeás un guard funcional?

typescript
describe('authGuard', () => {
  let authService: jasmine.SpyObj<AuthService>;
  let router: Router;

  beforeEach(() => {
    const authSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn']);

    TestBed.configureTestingModule({
      providers: [
        provideRouter([
          { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] },
          { path: 'login', component: LoginComponent }
        ]),
        { provide: AuthService, useValue: authSpy }
      ]
    });

    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
    router = TestBed.inject(Router);
  });

  it('should allow access when logged in', fakeAsync(() => {
    authService.isLoggedIn.and.returnValue(true);
    router.navigate(['/dashboard']);
    tick();
    expect(router.url).toBe('/dashboard');
  }));

  it('should redirect to login when not authenticated', fakeAsync(() => {
    authService.isLoggedIn.and.returnValue(false);
    router.navigate(['/dashboard']);
    tick();
    expect(router.url).toContain('/login');
  }));
});

40. ¿Qué diferencias hay entre `TestBed` y los tests unitarios puros?

Por qué la hacen: no todos los tests necesitan TestBed. Usarlo cuando no es necesario hace los tests lentos.

TestBed crea un módulo Angular de test — tiene overhead. Para testear lógica de negocio pura en un servicio, no necesitás TestBed:

typescript
// Test puro (sin TestBed) — más rápido
describe('CartService - pure', () => {
  let service: CartService;

  beforeEach(() => {
    service = new CartService(); // instanciación directa si no tiene deps
  });

  it('should calculate total correctly', () => {
    service.items.set([
      { id: 1, price: 100, qty: 2 },
      { id: 2, price: 50, qty: 1 }
    ]);
    expect(service.total()).toBe(250);
  });
});

Usá TestBed cuando necesitás el árbol de DI, el DOM, o el ciclo de vida de Angular. Para lógica pura, instanciá directamente.


Consejos finales para la entrevista

Lo que más diferencia a los candidatos:

  1. 1Hablar de trade-offs, no solo de features. Cuando te preguntan sobre OnPush, no alcanza con decir qué es — tenés que saber cuándo no conviene usarlo.
  1. 2Saber la evolución del framework. Angular 14 trajo standalone, 16 trajo signals experimentales, 17 los estabilizó, 18 trajo zoneless. Mostrar que seguís las versiones demuestra que realmente trabajás con Angular.
  1. 3Los errores comunes te revelan. Si explicás un error que cometiste vos mismo y cómo lo resolviste, eso es mucho más valioso que recitar la documentación.
  1. 4Tener opiniones justificadas. ¿NgRx vs signals para estado compartido? No existe la respuesta única — pero sí existe una respuesta pensada. El entrevistador quiere ver tu proceso de decisión.
  1. 5Preguntar sobre el stack del equipo. Si el equipo usa NgRx en v16 con módulos, preguntá cómo están migrando (o si planean migrar). Muestra que pensás en código de producción real, no en ejemplos de tutorial.

*¿Querés practicar estas preguntas con feedback en tiempo real? En InterviewHack.ai simulamos la entrevista técnica completa con análisis de tus respuestas y un dossier personalizado según el rol que buscás.*

FAQ

¿Cuáles son las preguntas más comunes de Angular en entrevistas técnicas?+

Las preguntas que más se repiten son: diferencia entre change detection Default vs OnPush, cómo funcionan switchMap/mergeMap/exhaustMap en RxJS, qué son los Angular Signals y cómo reemplazan a BehaviorSubject, cómo se implementa lazy loading, y cómo funciona el sistema de inyección de dependencias con sus distintos scopes.

¿Es necesario saber NgRx para conseguir trabajo con Angular?+

No es obligatorio, pero sí esperado en roles senior. Lo que sí te preguntan siempre es cuándo tiene sentido usar NgRx vs un servicio con signals o BehaviorSubject. Saber justificar cuándo no usarlo es igual de importante que saber usarlo.

¿Qué es OnPush change detection en Angular?+

OnPush es una estrategia de detección de cambios donde Angular solo re-renderiza el componente cuando recibe un nuevo objeto por @Input() (nueva referencia), cuando ocurre un evento dentro del componente, o cuando un Observable con async pipe emite. Evita recorrer el árbol completo en cada ciclo, mejorando significativamente el performance en listas grandes o componentes que no cambian frecuentemente.

¿Qué son los Angular Signals y para qué sirven?+

Los Signals son una primitiva reactiva introducida en Angular 16 (estable en v17) que envuelve valores y notifica automáticamente al framework cuando cambian. Resuelven el problema de Zone.js y el change detection grueso: Angular sabe exactamente qué cambió y actualiza solo esa parte del DOM. Son más simples que RxJS para estado local y no requieren unsubscribe.

¿Cómo evito memory leaks en Angular con RxJS?+

Hay tres formas: usar el async pipe en el template (Angular maneja la subscripción y cancelación automáticamente), usar takeUntilDestroyed(inject(DestroyRef)) en Angular 16+ que se cancela cuando el componente se destruye, o el patrón clásico con un Subject y takeUntil(this.destroy$) combinado con next()/complete() en ngOnDestroy.

¿Cuánto tiempo tengo que estudiar para una entrevista técnica de Angular?+

Depende del nivel. Para roles mid-level con 1-2 años de Angular, enfocate en change detection, RxJS básico (switchMap, combineLatest, memory leaks) e inyección de dependencias — con 1-2 semanas de repaso llegás bien. Para roles senior, sumá NgRx, signals, performance tuning, testing con TestBed y arquitectura de módulos vs standalone. Ahí necesitás experiencia práctica además del repaso teórico.

Related articles

Cómo conseguir trabajo remoto de TypeScript en dólares desde LATAM

Descubre cómo aplicar efectivamente a empleos remotos en TypeScript desde LATAM, destacar entre candidatos globales y negociar salarios en dólares.

Preguntas de entrevista de TypeScript con respuestas (45+)

Las 45+ preguntas de TypeScript más frecuentes en entrevistas técnicas, con respuestas detalladas y código real. De junior a senior, en español.

Preguntas de entrevista de TypeScript con respuestas (45+)

Las 45+ preguntas de TypeScript más frecuentes en entrevistas técnicas, con respuestas detalladas y código real. De junior a senior, en español rioplatense.

Prepare for your real interview

Paste your job link: we research who's interviewing you and rehearse you live.

Start free →

Have an interview coming up? Install the live copilot →

InterviewHack.ai

Prepare for the exact interview: who's interviewing you, a tailored CV, and a real coach.

Product

JobsFree ATS checkerInterview-English checkSalary checkLATAM salary reportFree coursesBlogTailored CVSpoken practiceIt's free

Remote jobs

ReactPythonFull-StackLATAMArgentinaMexicoSee all →

Prepare

Spoken practiceFrontendBackendAI EngineerBy companySell with your CV

Company

For employersAboutContactPrivacyTerms

© 2026 InterviewHack.ai · Your CV is yours. Never used to train anything. · A product of IA-PTY