diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html
index cc96faf4863cc775939491acf27205571d0e7dd1..bc7ed586bdd1d2a93cfd116180b1f526570d66d5 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.html
@@ -18,12 +18,12 @@
 
   <!-- Provisional model warning -->
   <nshmp-lib-ng-hazard-provisional-model
-    [name]="(facade.usageModelA$ | async)?.response?.model?.name"
+    [name]="facade.usageModelA()?.response?.model?.name"
   />
 
   <!-- Provisional model warning -->
   <nshmp-lib-ng-hazard-provisional-model
-    [name]="(facade.usageModelB$ | async)?.response?.model?.name"
+    [name]="facade.usageModelB()?.response?.model?.name"
   />
 
   <!-- About page -->
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.spec.ts
index ed7e68cdb2a13134d56a4082a7956654aa156e18..9809620ec6eab3096234083cb5557222b12b1745 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/app.component.spec.ts
@@ -2,7 +2,6 @@ import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
 import {provideRouter} from '@angular/router';
-import {provideMockStore} from '@ngrx/store/testing';
 
 import {AppComponent} from './app.component';
 import {AboutComponent} from './components/about/about.component';
@@ -34,7 +33,6 @@ describe('AppComponent', () => {
         ParameterSummaryComponent,
       ],
       providers: [
-        provideMockStore({initialState: {}}),
         provideHttpClient(),
         provideNoopAnimations(),
         provideRouter([]),
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html
index 66789e92d33da64912291468d6bb9eccfcbc8468..cc0344672b48a9bc32f16ed24f197f5583b75f03 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.html
@@ -7,17 +7,14 @@
   </mat-tab>
 
   <!-- Data tab -->
-  <mat-tab label="Hazard Curve Data" [disabled]="(hasData$ | async) === false">
+  <mat-tab label="Hazard Curve Data" [disabled]="hasData() === false">
     <ng-template matTabContent>
       <app-hazard-data />
     </ng-template>
   </mat-tab>
 
   <!-- Spectra tab -->
-  <mat-tab
-    label="Response Spectrum Data"
-    [disabled]="(hasData$ | async) === false"
-  >
+  <mat-tab label="Response Spectrum Data" [disabled]="hasData() === false">
     <ng-template matTabContent>
       <app-spectra-data />
     </ng-template>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.spec.ts
index 3c7ea105abe149d229ee4159046bfe9eae280029..a02bce78bf9b9f4a786db9b0bb7913fb4daadfda 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.spec.ts
@@ -1,6 +1,7 @@
+import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideRouter} from '@angular/router';
 
 import {ContentComponent} from './content.component';
 
@@ -12,8 +13,9 @@ describe('ContentComponent', () => {
     await TestBed.configureTestingModule({
       imports: [ContentComponent],
       providers: [
-        provideMockStore({initialState: {}}),
         provideNoopAnimations(),
+        provideHttpClient(),
+        provideRouter([]),
       ],
     }).compileComponents();
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts
index 895cf79bb35adcb91fcf64d540b15d810474582d..d66be9f4bbe2c02e7ad7bb2621c82e7e0afd7161 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/content/content.component.ts
@@ -1,7 +1,6 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {MatTab, MatTabContent, MatTabGroup} from '@angular/material/tabs';
-import {map} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {HazardDataComponent} from '../hazard-data/hazard-data.component';
@@ -31,13 +30,14 @@ import {SpectraDataComponent} from '../spectra-data/spectra-data.component';
 })
 export class ContentComponent {
   /** Whether there is data */
-  hasData$ = this.facade.serviceResponses$.pipe(
-    map(
-      models =>
-        models.modelA?.hazardResponse !== undefined &&
-        models.modelB?.hazardResponse !== undefined
-    )
-  );
+  hasData = computed(() => {
+    const {modelA, modelB} = this.facade.serviceResponses();
+
+    return (
+      modelA?.hazardResponse !== undefined &&
+      modelB?.hazardResponse !== undefined
+    );
+  });
 
   constructor(private facade: AppFacade) {}
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html
index 3bf9b85de0912e3c6704cfd4307b8cf381e37220..a1a3b1e134375a2639fbd258fc3d0b4f027bef89 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.html
@@ -1,100 +1,95 @@
 <!-- Control Panel -->
-@if (formState$ | async; as formState) {
-  <form
-    class="height-full overflow-auto"
-    [ngrxFormState]="formState"
-    (submit)="onSubmit()"
-  >
-    <!-- Model -->
-    <nshmp-lib-ng-hazard-model-form
-      [controlState]="formState?.controls?.model"
-      [models]="facade.availableModels$ | async"
-    />
+<form
+  class="height-full overflow-auto"
+  [formGroup]="formGroup"
+  (submit)="onSubmit()"
+>
+  <!-- Model -->
+  <nshmp-lib-no-ngrx-hazard-model-form
+    [modelControl]="formGroup.controls.model"
+    [models]="facade.availableModels()"
+  />
 
-    <!-- Model Compare -->
-    <nshmp-lib-ng-hazard-model-form
-      [controlState]="formState?.controls?.modelCompare"
-      [models]="facade.comparableModels$ | async"
-      label="Model Compare"
-      placeHolderText="--- Select a Different Model ---"
-    />
+  <!-- Model Compare -->
+  <nshmp-lib-no-ngrx-hazard-model-form
+    [modelControl]="formGroup.controls.modelCompare"
+    [models]="facade.comparableModels()"
+    label="Model Compare"
+    placeHolderText="--- Select a Different Model ---"
+  />
 
+  @if (facade.usage()) {
     <!-- Latitude -->
-    <nshmp-lib-ng-hazard-location-form
+    <nshmp-lib-no-ngrx-hazard-location-form
       class="latitude-input"
-      [controlState]="formState?.controls?.latitude"
+      [locationControl]="formGroup.controls.latitude"
+      [modelControl]="formGroup.controls.model"
       label="Latitude"
-      [bounds]="(facade.usage$ | async)?.response?.latitude"
+      [bounds]="facade.usage().response.latitude"
     />
 
     <!-- Longitude -->
-    <nshmp-lib-ng-hazard-location-form
+    <nshmp-lib-no-ngrx-hazard-location-form
       class="longitude-input"
-      [controlState]="formState?.controls?.longitude"
+      [locationControl]="formGroup.controls.longitude"
+      [modelControl]="formGroup.controls.model"
       label="Longitude"
-      [bounds]="(facade.usage$ | async)?.response?.longitude"
+      [bounds]="facade.usage().response.longitude"
     />
+  }
 
-    <!-- Select site -->
-    <nshmp-lib-ng-map-select-site
-      [data]="selectSiteData$ | async"
-      (siteSelect)="facade.setLocation($event)"
-    />
+  <!-- Select site -->
+  <nshmp-lib-no-ngrx-map-select-site
+    [data]="selectSiteData()"
+    (siteSelect)="facade.setLocation($event)"
+  />
 
-    <!-- Imt -->
-    <mat-form-field class="grid-col-12">
-      <mat-label>Intensity Measure Type</mat-label>
-      <mat-select [ngrxFormControlState]="formState?.controls?.imt">
-        @for (imt of imts$ | async; track imt) {
-          <mat-option [value]="imt.value">
-            {{ imt.display }}
-          </mat-option>
-        }
-      </mat-select>
-    </mat-form-field>
+  <!-- Imt -->
+  <mat-form-field class="grid-col-12">
+    <mat-label>Intensity Measure Type</mat-label>
+    <mat-select [formControl]="formGroup.controls.imt">
+      @for (imt of imts(); track imt) {
+        <mat-option [value]="imt.value">
+          {{ imt.display }}
+        </mat-option>
+      }
+    </mat-select>
+  </mat-form-field>
 
-    <!-- Site Class -->
-    <div class="grid-row grid-gap-sm">
-      <!-- Site classes -->
-      <nshmp-lib-ng-hazard-site-class-form
-        class="grid-col-7"
-        [controlState]="formState?.controls?.siteClass"
-        [siteClasses]="siteClasses$ | async"
-        formFieldClass="grid-col-12"
+  <!-- Site Class -->
+  <div class="grid-row grid-gap-sm">
+    <!-- Site classes -->
+    @if (siteClasses()) {
+      <nshmp-lib-no-ngrx-hazard-site-class-form
+        [siteClassControl]="formGroup.controls.siteClass"
+        [siteClasses]="siteClasses()"
+        [modelControl]="formGroup.controls.model"
+        [vs30Control]="formGroup.controls.vs30"
+        [vs30Bounds]="facade.usage()?.response.vs30"
       />
+    }
+  </div>
 
-      <!-- Vs30-->
-      <nshmp-lib-ng-hazard-vs30-form
-        class="grid-col-5"
-        [controlState]="formState?.controls?.vs30"
-        [bounds]="(facade.usage$ | async)?.vs30"
-        formFieldClass="grid-col-12"
-      />
-    </div>
+  <!-- Return Period Controls -->
+  <nshmp-lib-no-ngrx-hazard-return-period-form
+    [returnPeriodControl]="formGroup.controls.returnPeriod"
+    [commonReturnPeriodControl]="formGroup.controls.commonReturnPeriods"
+  />
 
-    <!-- Return Period Controls -->
-    <nshmp-lib-ng-hazard-return-period-form
-      [returnPeriodControlState]="formState?.controls?.returnPeriod"
-      [commonReturnPeriodControlState]="
-        formState?.controls?.commonReturnPeriods
-      "
-    />
+  <!-- Truncation -->
+  <nshmp-lib-no-ngrx-hazard-truncation-form
+    [truncationControl]="formGroup.controls.truncate"
+  />
 
-    <!-- Truncation -->
-    <nshmp-lib-ng-hazard-truncation-form
-      [controlState]="formState?.controls?.truncate"
-    />
-
-    <!-- Max Direction -->
-    <nshmp-lib-ng-hazard-max-direction-form
-      [controlState]="formState?.controls?.maxDirection"
-    />
+  <!-- Max Direction -->
+  <nshmp-lib-no-ngrx-hazard-max-direction-form
+    [maxDirectionControl]="formGroup.controls.maxDirection"
+  />
 
-    <nshmp-lib-ng-control-panel-buttons
-      [plotDisabled]="formState?.isInvalid"
-      [serviceCallInfo]="facade.serviceCallInfo$ | async"
-      [resetDisabled]="formState?.isPristine"
-      (resetClick)="facade.resetControlPanel()"
-    />
-  </form>
-}
+  <nshmp-lib-no-ngrx-control-panel-buttons
+    [plotDisabled]="formGroup.invalid"
+    [serviceCallInfo]="facade.serviceCallInfo()"
+    [resetDisabled]="formGroup.pristine"
+    (resetClick)="facade.resetControlPanel()"
+  />
+</form>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.spec.ts
index 17b38b87c795f14adb6a7164459f488b9a119b06..dcbdc85c343b5a6ab603b0bae813fc69662deeb7 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.spec.ts
@@ -1,6 +1,7 @@
 import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideNoopAnimations} from '@angular/platform-browser/animations';
+import {provideRouter} from '@angular/router';
 
 import {ControlPanelComponent} from './control-panel.component';
 
@@ -11,7 +12,11 @@ describe('ControlPanelComponent', () => {
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       imports: [ControlPanelComponent],
-      providers: [provideMockStore({initialState: {}}), provideHttpClient()],
+      providers: [
+        provideHttpClient(),
+        provideRouter([]),
+        provideNoopAnimations(),
+      ],
     }).compileComponents();
 
     fixture = TestBed.createComponent(ControlPanelComponent);
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts
index 1af22eebf94b378d005286ed8aa6ea3e60343e67..ce26f18abe6a1aec150d12cb8cbb0844ca7e6f4b 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/control-panel/control-panel.component.ts
@@ -1,34 +1,35 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed, OnDestroy, OnInit, Signal} from '@angular/core';
+import {ReactiveFormsModule} from '@angular/forms';
 import {MatOption} from '@angular/material/core';
-import {MatFormField, MatLabel} from '@angular/material/form-field';
+import {MatError, MatFormField, MatLabel} from '@angular/material/form-field';
+import {MatInput} from '@angular/material/input';
 import {MatSelect} from '@angular/material/select';
 import {
-  hazardUtils,
   NshmpLibNgHazardLocationFormComponent,
   NshmpLibNgHazardMaxDirectionFormComponent,
   NshmpLibNgHazardModelFormComponent,
   NshmpLibNgHazardReturnPeriodFormComponent,
   NshmpLibNgHazardSiteClassFormComponent,
   NshmpLibNgHazardTruncationFormComponent,
-  NshmpLibNgHazardVs30FormComponent,
-} from '@ghsc/nshmp-lib-ng/hazard';
+} from '@ghsc/nshmp-lib-no-ngrx/hazard';
 import {
   NshmpLibNgMapSelectSiteComponent,
   SelectSiteDialogData,
-} from '@ghsc/nshmp-lib-ng/map';
+} from '@ghsc/nshmp-lib-no-ngrx/map';
 import {
   NshmpLibNgControlPanelButtonsComponent,
-  NshmpNgrxFormsModule,
   NshmpService,
   nshmpUtils,
   RETURN_PERIOD_BOUNDS,
   RETURN_PERIODS,
-} from '@ghsc/nshmp-lib-ng/nshmp';
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {environment} from 'projects/nshmp-apps/src/environments/environment';
-import {combineLatest, map, Observable} from 'rxjs';
+import {Subscription} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
+import {combineUsages} from '../../utils/response-handler.utils';
 
 /**
  * Control panel with form fields to call dynamic hazard services.
@@ -40,23 +41,24 @@ import {AppFacade} from '../../state/app.facade';
     MatSelect,
     MatOption,
     AsyncPipe,
+    MatInput,
     NshmpLibNgHazardModelFormComponent,
     NshmpLibNgHazardLocationFormComponent,
     NshmpLibNgMapSelectSiteComponent,
     NshmpLibNgHazardSiteClassFormComponent,
-    NshmpLibNgHazardVs30FormComponent,
     NshmpLibNgHazardReturnPeriodFormComponent,
     NshmpLibNgHazardTruncationFormComponent,
     NshmpLibNgHazardMaxDirectionFormComponent,
     NshmpLibNgControlPanelButtonsComponent,
-    NshmpNgrxFormsModule,
+    ReactiveFormsModule,
+    MatError,
   ],
   selector: 'app-control-panel',
   standalone: true,
   styleUrl: './control-panel.component.scss',
   templateUrl: './control-panel.component.html',
 })
-export class ControlPanelComponent {
+export class ControlPanelComponent implements OnInit, OnDestroy {
   /** Max and min bounds for return periods */
   returnPeriodBounds = RETURN_PERIOD_BOUNDS;
   /** List of return periods */
@@ -65,45 +67,92 @@ export class ControlPanelComponent {
   siteClassPlaceHolder = nshmpUtils.selectPlaceHolder();
 
   /** Form field state */
-  formState$ = this.facade.controlPanelForm$;
+  formGroup = this.facade.formGroup;
 
   /** List of available IMTs */
-  imts$ = this.facade.usage$.pipe(map(usage => usage?.response?.model?.imts));
+  imts = computed(() => this.facade.usage()?.response?.model?.imts);
 
   /** Data for select site component */
-  selectSiteData$: Observable<SelectSiteDialogData> = combineLatest([
-    this.facade.usage$,
-    this.facade.controlPanelForm$,
-    this.facade.nshmService$,
-  ]).pipe(
-    map(([usage, form, nshmService]) => {
-      if (usage === null || nshmService === undefined) {
-        return;
-      }
-
-      const services =
-        environment.webServices.nshmpHazWs.services.curveServices;
-
-      return {
-        mapUrl: `${nshmService.url}${services.map}?raw=true`,
-        nshm: form.value.model,
-        sitesUrl: `${nshmService.url}${services.sites}?raw=true`,
-      };
-    })
-  );
+  selectSiteData: Signal<SelectSiteDialogData> = computed(() => {
+    const usage = this.facade.usage();
+    const nshmService = this.facade.nshmService();
+
+    if (usage === null || nshmService === undefined) {
+      return;
+    }
+
+    const services = environment.webServices.nshmpHazWs.services.curveServices;
+
+    return {
+      mapUrl: `${nshmService.url}${services.map}?raw=true`,
+      nshm: this.facade.formGroup.getRawValue().model,
+      sitesUrl: `${nshmService.url}${services.sites}?raw=true`,
+    };
+  });
 
   /** List of site class `Parameter`s */
-  siteClasses$ = this.facade.usage$.pipe(
-    map(usage =>
-      hazardUtils.siteClassesToParameters(usage?.response?.model?.siteClasses)
-    )
+  siteClasses = computed(
+    () => this.facade.usage()?.response?.model.siteClasses
   );
 
+  private imtSubscription = new Subscription();
+  private maxDirectionSubscription = new Subscription();
+  private modelCompareSubscription = new Subscription();
+  private modelSubscription = new Subscription();
+  private returnPeriodSubscription = new Subscription();
+  private truncateSubscription = new Subscription();
+  private vs30Subscription = new Subscription();
+
   constructor(
     public facade: AppFacade,
     private nshmpService: NshmpService
   ) {}
 
+  ngOnInit(): void {
+    this.modelSubscription =
+      this.formGroup.controls.model.valueChanges.subscribe(() => {
+        this.facade.onModelChange();
+      });
+
+    this.modelCompareSubscription =
+      this.formGroup.controls.modelCompare.valueChanges.subscribe(
+        modelCompare => this.onModelCompare(modelCompare)
+      );
+
+    this.imtSubscription = this.formGroup.controls.imt.valueChanges.subscribe(
+      () => this.facade.createPlots()
+    );
+
+    this.maxDirectionSubscription =
+      this.formGroup.controls.maxDirection.valueChanges.subscribe(() =>
+        this.facade.createPlots()
+      );
+
+    this.returnPeriodSubscription =
+      this.formGroup.controls.returnPeriod.valueChanges.subscribe(() =>
+        this.facade.createPlots()
+      );
+
+    this.truncateSubscription =
+      this.formGroup.controls.truncate.valueChanges.subscribe(() =>
+        this.facade.createPlots()
+      );
+
+    this.vs30Subscription = this.formGroup.controls.vs30.valueChanges.subscribe(
+      () => this.facade.resetState()
+    );
+  }
+
+  ngOnDestroy(): void {
+    this.imtSubscription.unsubscribe();
+    this.maxDirectionSubscription.unsubscribe();
+    this.modelCompareSubscription.unsubscribe();
+    this.modelSubscription.unsubscribe();
+    this.returnPeriodSubscription.unsubscribe();
+    this.truncateSubscription.unsubscribe();
+    this.vs30Subscription.unsubscribe();
+  }
+
   /**
    * On form submit.
    */
@@ -111,4 +160,16 @@ export class ControlPanelComponent {
     this.facade.callServices();
     this.nshmpService.selectPlotControl();
   }
+
+  private onModelCompare(modelCompare: NshmId): void {
+    if (modelCompare === null) {
+      this.facade.onModelChange();
+    } else {
+      const usages = [this.facade.usageModelA(), this.facade.usageModelB()];
+
+      this.facade.updateState({
+        combinedUsage: combineUsages(usages),
+      });
+    }
+  }
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html
index 16a791208cc5f59fc2c18df32a23339e39ed7e56..786dd8646e3c858c78d961d5fa37a3d4b7e3c287 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.html
@@ -1,20 +1,20 @@
 <div class="grid-container-widescreen-xl margin-y-3">
   <mat-accordion multi>
     <app-table-data-panel
-      [filename]="hazardFilename$ | async"
-      [tableData]="hazardTableData$ | async"
+      [filename]="hazardFilename"
+      [tableData]="hazardTableData()"
       panelTitle="Hazard Curves: Total"
     />
 
     <app-table-data-panel
-      [filename]="hazardDiffFilename$ | async"
-      [tableData]="hazardDiffTableData$ | async"
+      [filename]="hazardDiffFilename"
+      [tableData]="hazardDiffTableData()"
       panelTitle="Hazard Curves: Percent Difference"
     />
 
     <app-table-data-panel
-      [filename]="hazardComponentsFilename$ | async"
-      [tableData]="hazardComponentsTableData$ | async"
+      [filename]="hazardComponentsFilename"
+      [tableData]="hazardComponentsTableData()"
       panelTitle="Hazard Curves: Components"
     />
   </mat-accordion>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.spec.ts
index b82e6f9c6da2cad266cbc141500beec917c4eb13..b59cd9f4b8400fbd7d3cd9694af7c77edc83425d 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.spec.ts
@@ -1,7 +1,7 @@
 import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideRouter} from '@angular/router';
 
 import {HazardDataComponent} from './hazard-data.component';
 
@@ -13,9 +13,9 @@ describe('HazardDataComponent', () => {
     await TestBed.configureTestingModule({
       imports: [HazardDataComponent],
       providers: [
-        provideMockStore({initialState: {}}),
         provideHttpClient(),
         provideNoopAnimations(),
+        provideRouter([]),
       ],
     }).compileComponents();
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts
index 7f1bc6575be0f83b2d0436d23805faddd0b6746d..623e587be6e87934a50c515428569d63fc3854ac 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/hazard-data/hazard-data.component.ts
@@ -1,10 +1,9 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {MatAccordion} from '@angular/material/expansion';
-import {PlotTableDataParams, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
+import {PlotTableDataParams, plotUtils} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {Imt, imtToString} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
 import {PlotData} from 'plotly.js';
-import {map} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {Plots} from '../../state/app.state';
@@ -22,19 +21,15 @@ import {TableDataPanelComponent} from '../table-data-panel/table-data-panel.comp
 })
 export class HazardDataComponent {
   /** Hazard components plot data */
-  private hazardComponentsPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_COMPONENTS))
-  );
+  private hazardComponentsPlot = this.facade
+    .plots()
+    .get(Plots.HAZARD_COMPONENTS);
 
   /** Hazard % diff plot data */
-  private hazardDiffPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_DIFFERENCES))
-  );
+  private hazardDiffPlot = this.facade.plots().get(Plots.HAZARD_DIFFERENCES);
 
   /** Hazard plot data */
-  private hazardPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD))
-  );
+  private hazardPlot = this.facade.plots().get(Plots.HAZARD);
 
   /** Table data parameters */
   private tableParams: PlotTableDataParams = {
@@ -49,71 +44,57 @@ export class HazardDataComponent {
   };
 
   /** Model values */
-  private models$ = this.facade.controlPanelForm$.pipe(
-    map(form => `${form.value.model}-${form.value.modelCompare}`)
-  );
+  private models = `${this.facade.formGroup.getRawValue().model}-${this.facade.formGroup.getRawValue().modelCompare}`;
 
   /** Filename for hazard components export CSV */
-  hazardComponentsFilename$ = this.models$.pipe(
-    map(models => `hazard-components-${models}.csv`)
-  );
+  hazardComponentsFilename = `hazard-components-${this.models}.csv`;
 
   /** Filename for hazard export CSV */
-  hazardFilename$ = this.models$.pipe(
-    map(models => `hazard-compare-${models}.csv`)
-  );
+  hazardFilename = `hazard-compare-${this.models}.csv`;
 
   /** Filename for hazard difference export CSV */
-  hazardDiffFilename$ = this.models$.pipe(
-    map(models => `hazard-%-diff-${models}.csv`)
-  );
+  hazardDiffFilename = `hazard-%-diff-${this.models}.csv`;
 
   /** Hazard table data */
-  hazardTableData$ = this.hazardPlot$.pipe(
-    map(plot => {
-      const data = [...plot.plotData.data];
-      // Remove return period from data
-      data.shift();
+  hazardTableData = computed(() => {
+    const data = [...this.hazardPlot.plotData.data];
+    // Remove return period from data
+    data.shift();
 
-      return plotUtils.plotDataToTableData(
-        {
-          ...plot,
-          plotData: {
-            ...plot.plotData,
-            data,
-          },
+    return plotUtils.plotDataToTableData(
+      {
+        ...this.hazardPlot,
+        plotData: {
+          ...this.hazardPlot.plotData,
+          data,
         },
-        this.tableParams
-      );
-    })
-  );
+      },
+      this.tableParams
+    );
+  });
 
   /** Hazard difference table data */
-  hazardDiffTableData$ = this.hazardDiffPlot$.pipe(
-    map(plot => {
-      return plotUtils.plotDataToTableData(plot, this.tableParams);
-    })
+  hazardDiffTableData = computed(() =>
+    plotUtils.plotDataToTableData(this.hazardDiffPlot, this.tableParams)
   );
 
   /** Hazard components table data */
-  hazardComponentsTableData$ = this.hazardComponentsPlot$.pipe(
-    map(plot => {
-      const data = [...plot.plotData.data];
-      // Remove return period from data
-      data.shift();
+  hazardComponentsTableData = computed(() => {
+    const data = [...this.hazardComponentsPlot.plotData.data];
+    // Remove return period from data
+    data.shift();
 
-      return plotUtils.plotDataToTableData(
-        {
-          ...plot,
-          plotData: {
-            ...plot.plotData,
-            data,
-          },
+    return plotUtils.plotDataToTableData(
+      {
+        ...this.hazardComponentsPlot,
+        plotData: {
+          ...this.hazardComponentsPlot.plotData,
+          data,
         },
-        this.tableParams
-      );
-    })
-  );
+      },
+      this.tableParams
+    );
+  });
 
   constructor(private facade: AppFacade) {}
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html
index 2b1c1476e38f3faa9f6d22a094303d7ba01c8565..4689f8b4e662c99e5375b0b8d8d6913720f0e80c 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.html
@@ -1,69 +1,65 @@
-@if (form$ | async; as form) {
-  <div class="grid-row parameter-summary">
-    <div class="grid-col-12 tablet:grid-col-6">
-      <mat-list class="parameter-list">
-        <mat-list-item>
-          <span class="parameter">Model</span>:
-          {{ toDisplay(form.value.model, facade.availableModels$ | async) }}
-        </mat-list-item>
+<div class="grid-row parameter-summary">
+  <div class="grid-col-12 tablet:grid-col-6">
+    <mat-list class="parameter-list">
+      <mat-list-item>
+        <span class="parameter">Model</span>:
+        {{ toDisplay(form.value.model, facade.availableModels()) }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Model Compare</span>:
-          {{
-            toDisplay(form.value.modelCompare, facade.availableModels$ | async)
-          }}
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Model Compare</span>:
+        {{ toDisplay(form.value.modelCompare, facade.availableModels()) }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Latitude</span>: {{ form.value.latitude }}°
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Latitude</span>: {{ form.value.latitude }}°
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Longitude</span>: {{ form.value.longitude }}°
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Longitude</span>: {{ form.value.longitude }}°
+      </mat-list-item>
 
-        <mat-list-item>
-          @if (form.value.siteClass !== selectPlaceHolder.value) {
-            <span class="parameter">Site Class</span>:
-            {{ toDisplay(form.value.siteClass, siteClasses$ | async) }}
-          } @else {
-            <span class="parameter">Vs30</span>: {{ form.value.vs30 }} m/s
-          }
-        </mat-list-item>
-      </mat-list>
-    </div>
+      <mat-list-item>
+        @if (form.value.siteClass !== selectPlaceHolder.value) {
+          <span class="parameter">Site Class</span>:
+          {{ toDisplay(form.value.siteClass, siteClasses$ | async) }}
+        } @else {
+          <span class="parameter">Vs30</span>: {{ form.value.vs30 }} m/s
+        }
+      </mat-list-item>
+    </mat-list>
+  </div>
 
-    <div class="grid-col-12 tablet:grid-col-6 print-flex-basis-half">
-      <mat-list class="parameter-list">
-        <mat-list-item>
-          <span class="parameter">IMT</span>:
-          {{ form.value.imt }}
-        </mat-list-item>
+  <div class="grid-col-12 tablet:grid-col-6 print-flex-basis-half">
+    <mat-list class="parameter-list">
+      <mat-list-item>
+        <span class="parameter">IMT</span>:
+        {{ form.value.imt }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Return Period</span>:
-          @if (form.value.commonReturnPeriod !== selectPlaceHolder.value) {
-            {{ returnPeriodName[form.value.returnPeriod] }}
-          } @else {
-            {{ form.value.returnPeriod }} yr
-          }
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Return Period</span>:
+        <!-- @if (form.getRawValue().returnPeriod !== selectPlaceHolder.value) {
+          {{ returnPeriodName[form.value.returnPeriod] }}
+        } @else {
+          {{ form.value.returnPeriod }} yr
+        } -->
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Source Type</span>:
-          {{ form.value.sourceType }}
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Source Type</span>:
+        {{ form.value.sourceType }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Truncate</span>:
-          {{ form.value.truncate === true ? 'On' : 'Off' }}
-        </mat-list-item>
+      <mat-list-item>
+        <span class="parameter">Truncate</span>:
+        {{ form.value.truncate === true ? 'On' : 'Off' }}
+      </mat-list-item>
 
-        <mat-list-item>
-          <span class="parameter">Max Direction</span>:
-          {{ form.value.maxDirection === true ? 'On' : 'Off' }}
-        </mat-list-item>
-      </mat-list>
-    </div>
+      <mat-list-item>
+        <span class="parameter">Max Direction</span>:
+        {{ form.value.maxDirection === true ? 'On' : 'Off' }}
+      </mat-list-item>
+    </mat-list>
   </div>
-}
+</div>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.spec.ts
index 10bfea4db08555fa96249b81d15dd82cba66a7b4..3642d0d1e9b31b6f64d0c505b3ad5a45af58edad 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.spec.ts
@@ -1,5 +1,7 @@
+import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideNoopAnimations} from '@angular/platform-browser/animations';
+import {provideRouter} from '@angular/router';
 
 import {ParameterSummaryComponent} from './parameter-summary.component';
 
@@ -10,7 +12,11 @@ describe('ParameterSummaryComponent', () => {
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       imports: [ParameterSummaryComponent],
-      providers: [provideMockStore({initialState: {}})],
+      providers: [
+        provideHttpClient(),
+        provideNoopAnimations(),
+        provideRouter([]),
+      ],
     }).compileComponents();
 
     fixture = TestBed.createComponent(ParameterSummaryComponent);
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts
index 9ed74b4711ccf5a74a39e008f1d0f86562a04261..17c3004aeefc4ca6c4a27250e027e4e89ed91e68 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/parameter-summary/parameter-summary.component.ts
@@ -1,9 +1,8 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {MatList, MatListItem} from '@angular/material/list';
-import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {nshmpUtils, returnPeriodAltName} from '@ghsc/nshmp-lib-ng/nshmp';
-import {map} from 'rxjs';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {nshmpUtils, returnPeriodAltName} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 
 import {AppFacade} from '../../state/app.facade';
 
@@ -25,14 +24,20 @@ export class ParameterSummaryComponent {
   toDisplay = nshmpUtils.parameterToDisplay;
 
   /** Form field state */
-  form$ = this.facade.controlPanelForm$;
+  form = this.facade.formGroup;
 
   /** Site classes from usage as `Parameter`s */
-  siteClasses$ = this.facade.usage$.pipe(
-    map(usage =>
-      hazardUtils.siteClassesToParameters(usage?.response?.model?.siteClasses)
-    )
-  );
+  siteClasses = computed(() => {
+    const usage = this.facade.usage();
 
-  constructor(private facade: AppFacade) {}
+    if (usage === undefined || usage === null) {
+      return [];
+    }
+
+    return hazardUtils.siteClassesToParameters(
+      usage.response.model.siteClasses
+    );
+  });
+
+  constructor(public facade: AppFacade) {}
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html
index 31daed70807f3d906515b047eb2163bae00dd805..8dfad0ad328f5207a5aa9ecface99524146296a5 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.html
@@ -5,7 +5,10 @@
         <mat-expansion-panel-header>
           <mat-panel-title>{{ panel.title }}</mat-panel-title>
         </mat-expansion-panel-header>
-        <nshmp-lib-ng-plot-settings [plot]="panel.plot$ | async" />
+        <nshmp-lib-no-ngrx-plot-settings
+          [plot]="panel.plot()"
+          (updatedPlot)="updatePlot(panel.id, $event)"
+        />
       </mat-expansion-panel>
     }
   </mat-accordion>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.spec.ts
index 444a23c17df0dbd51a4a3bd80357501eb395a58c..29446243669399ceb07960abe2eede6a9c3289c2 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.spec.ts
@@ -1,6 +1,7 @@
+import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideRouter} from '@angular/router';
 
 import {PlotSettingsComponent} from './plot-settings.component';
 
@@ -12,8 +13,9 @@ describe('PlotSettingsComponent', () => {
     await TestBed.configureTestingModule({
       imports: [PlotSettingsComponent],
       providers: [
-        provideMockStore({initialState: {}}),
+        provideHttpClient(),
         provideNoopAnimations(),
+        provideRouter([]),
       ],
     }).compileComponents();
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts
index 6156988a8fd66e4d091b81b5be994a4ac53a6d74..4b262db0941e5e8d02d85422f22fed00cc5c2dda 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plot-settings/plot-settings.component.ts
@@ -1,5 +1,5 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed} from '@angular/core';
 import {
   MatAccordion,
   MatExpansionPanel,
@@ -8,9 +8,9 @@ import {
 } from '@angular/material/expansion';
 import {
   NshmpLibNgPlotSettingsComponent,
-  PlotSettingsPanel,
-} from '@ghsc/nshmp-lib-ng/plot';
-import {map} from 'rxjs';
+  NshmpPlot,
+} from '@ghsc/nshmp-lib-no-ngrx/plot';
+import {PlotSettingsPanelId} from 'projects/nshmp-apps/src/shared/models/plot-settings-panel.model';
 
 import {AppFacade} from '../../state/app.facade';
 import {Plots} from '../../state/app.state';
@@ -34,62 +34,73 @@ import {Plots} from '../../state/app.state';
 })
 export class PlotSettingsComponent {
   /** Hazard plot data */
-  private hazardPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD))
-  );
+  private hazardPlot = computed(() => this.facade.plots().get(Plots.HAZARD));
 
   /** Hazard components plot data */
-  private hazardComponentsPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_COMPONENTS))
+  private hazardComponentsPlot = computed(() =>
+    this.facade.plots().get(Plots.HAZARD_COMPONENTS)
   );
 
   /** Hazard % diff plot data */
-  private hazardDiffPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_DIFFERENCES))
+  private hazardDiffPlot = computed(() =>
+    this.facade.plots().get(Plots.HAZARD_DIFFERENCES)
   );
 
   /** Spectra components plot data */
-  private spectraComponentsPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_COMPONENTS))
+  private spectraComponentsPlot = computed(() =>
+    this.facade.plots().get(Plots.SPECTRUM_COMPONENTS)
   );
 
   /** Spectra % diff plot data */
-  private spectraDiffPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_DIFFERENCES))
+  private spectraDiffPlot = computed(() =>
+    this.facade.plots().get(Plots.SPECTRUM_DIFFERENCES)
   );
 
   /** Spectra plot data */
-  private spectraPlot$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM))
-  );
+  private spectraPlot = computed(() => this.facade.plots().get(Plots.SPECTRUM));
 
   /** The panels with plot settings */
-  panels: PlotSettingsPanel[] = [
+  panels: PlotSettingsPanelId[] = [
     {
-      plot$: this.hazardPlot$,
+      id: Plots.HAZARD,
+      plot: this.hazardPlot,
       title: 'Hazard Curves Plot Settings',
     },
     {
-      plot$: this.hazardDiffPlot$,
+      id: Plots.HAZARD_DIFFERENCES,
+      plot: this.hazardDiffPlot,
       title: 'Hazard % Difference Plot Settings',
     },
     {
-      plot$: this.spectraPlot$,
+      id: Plots.SPECTRUM,
+      plot: this.spectraPlot,
       title: 'Spectra Plot Settings',
     },
     {
-      plot$: this.spectraDiffPlot$,
+      id: Plots.SPECTRUM_DIFFERENCES,
+      plot: this.spectraDiffPlot,
       title: 'Spectra % Difference Plot Settings',
     },
     {
-      plot$: this.hazardComponentsPlot$,
+      id: Plots.HAZARD_COMPONENTS,
+      plot: this.hazardComponentsPlot,
       title: 'Hazard Component Curves Plot Settings',
     },
     {
-      plot$: this.spectraComponentsPlot$,
+      id: Plots.SPECTRUM_COMPONENTS,
+      plot: this.spectraComponentsPlot,
       title: 'Spectra Component Curves Plot Settings',
     },
   ];
 
   constructor(private facade: AppFacade) {}
+
+  updatePlot(id: string, plot: NshmpPlot): void {
+    console.log(id, plot);
+    const plots = new Map(this.facade.state().plots);
+    plots.set(id, plot);
+    this.facade.updateState({
+      plots,
+    });
+  }
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html
index 184bd1f0660fb3885f5ec700efe7616fe15b1f06..256fa41fc70374bc3503878e0a0ebb16a660da5b 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.html
@@ -1,5 +1,7 @@
-<nshmp-lib-ng-plots-container containerClass="grid-container-widescreen-xl">
-  <nshmp-lib-ng-hazard-design-code-warning />
+<nshmp-lib-no-ngrx-plots-container
+  containerClass="grid-container-widescreen-xl"
+>
+  <nshmp-lib-no-ngrx-hazard-design-code-warning />
 
   <div class="grid-row">
     @for (plot of plots; track plot) {
@@ -16,13 +18,13 @@
               <mat-panel-title>{{ plot.title }}</mat-panel-title>
             </mat-expansion-panel-header>
             <mat-divider />
-            @if (plot.plotData$ | async; as plotData) {
-              <nshmp-lib-ng-plot [plot]="plotData" />
-            }
-            @if (plot.diffPlotData$ | async; as diffPlotData) {
+
+            <nshmp-lib-no-ngrx-plot [plot]="plot.plotData()" />
+
+            @if (plot.diffPlotData) {
               <div class="padding-top-4">
                 <mat-divider />
-                <nshmp-lib-ng-plot [plot]="diffPlotData" />
+                <nshmp-lib-no-ngrx-plot [plot]="plot.diffPlotData()" />
               </div>
             }
           </mat-expansion-panel>
@@ -49,9 +51,9 @@
           </mat-expansion-panel-header>
 
           <mat-divider />
-          <nshmp-lib-ng-app-metadata [repositories]="repositories$ | async" />
+          <nshmp-lib-no-ngrx-app-metadata [repositories]="repositories()" />
         </mat-expansion-panel>
       </mat-accordion>
     </div>
   </div>
-</nshmp-lib-ng-plots-container>
+</nshmp-lib-no-ngrx-plots-container>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.spec.ts
index e562e2d316dbc6e5c6968468fe2ecbe0b704f44f..df5874f64cde103957e857959fb56fc80181bef3 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.spec.ts
@@ -1,7 +1,7 @@
 import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideRouter} from '@angular/router';
 
 import {PlotsComponent} from './plots.component';
 
@@ -13,7 +13,7 @@ describe('PlotsComponent', () => {
     await TestBed.configureTestingModule({
       imports: [PlotsComponent],
       providers: [
-        provideMockStore({initialState: {}}),
+        provideRouter([]),
         provideHttpClient(),
         provideNoopAnimations(),
       ],
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts
index 206fc18f6edc1a9ee46040af196ffea67a1bf748..89210f5d8fdaab491f0a0c290b29197e9664897d 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/plots/plots.component.ts
@@ -1,5 +1,5 @@
 import {AsyncPipe, NgClass} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed, Signal} from '@angular/core';
 import {MatDivider} from '@angular/material/divider';
 import {
   MatAccordion,
@@ -7,15 +7,15 @@ import {
   MatExpansionPanelHeader,
   MatExpansionPanelTitle,
 } from '@angular/material/expansion';
-import {NshmpLibNgHazardDesignCodeWarningComponent} from '@ghsc/nshmp-lib-ng/hazard';
-import {NshmpLibNgAppMetadataComponent} from '@ghsc/nshmp-lib-ng/nshmp';
+import {NshmpLibNgHazardDesignCodeWarningComponent} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {NshmpLibNgAppMetadataComponent} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 import {
   NshmpLibNgPlotComponent,
   NshmpLibNgPlotsContainerComponent,
-} from '@ghsc/nshmp-lib-ng/plot';
+} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {AppControlsService} from '@ghsc/nshmp-template';
 import {PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
-import {map, Observable} from 'rxjs';
+import {map} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {Plots} from '../../state/app.state';
@@ -26,12 +26,12 @@ import {ParameterSummaryComponent} from '../parameter-summary/parameter-summary.
  */
 interface PlotInfo {
   /** Main plot data */
-  plotData$: Observable<PlotlyPlot>;
+  plotData: Signal<PlotlyPlot>;
   /** Expansion panel title */
   title: string;
 
   /** % diff plot data */
-  diffPlotData$?: Observable<PlotlyPlot>;
+  diffPlotData?: Signal<PlotlyPlot>;
 }
 
 /**
@@ -58,34 +58,34 @@ interface PlotInfo {
   templateUrl: './plots.component.html',
 })
 export class PlotsComponent {
-  /** Hazard components plot data */
-  private hazardComponentsPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_COMPONENTS)?.plotData)
+  /** Hazard plot data */
+  private hazardPlotData = computed(
+    () => this.facade.plots().get(Plots.HAZARD).plotData
   );
 
-  /** Hazard % diff plot data */
-  private hazardDiffPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD_DIFFERENCES)?.plotData)
+  /** Hazard components plot data */
+  private hazardComponentsPlotData = computed(
+    () => this.facade.plots().get(Plots.HAZARD_COMPONENTS).plotData
   );
 
-  /** Hazard plot data */
-  private hazardPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.HAZARD)?.plotData)
+  /** Hazard % diff plot data */
+  private hazardDiffPlotData = computed(
+    () => this.facade.plots().get(Plots.HAZARD_DIFFERENCES).plotData
   );
 
   /** Spectra components plot data */
-  private spectraComponentsPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_COMPONENTS)?.plotData)
+  private spectraComponentsPlotData = computed(
+    () => this.facade.plots().get(Plots.SPECTRUM_COMPONENTS).plotData
   );
 
   /** Spectra % diff plot data */
-  private spectraDiffPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM_DIFFERENCES)?.plotData)
+  private spectraDiffPlotData = computed(
+    () => this.facade.plots().get(Plots.SPECTRUM_DIFFERENCES).plotData
   );
 
   /** Spectra plot data */
-  private spectraPlotData$ = this.facade.plots$.pipe(
-    map(plots => plots.get(Plots.SPECTRUM)?.plotData)
+  private spectraPlotData = computed(
+    () => this.facade.plots().get(Plots.SPECTRUM).plotData
   );
 
   /** Wheather both the control and settings panels are closed */
@@ -109,29 +109,27 @@ export class PlotsComponent {
   /** The plots */
   plots: PlotInfo[] = [
     {
-      diffPlotData$: this.hazardDiffPlotData$,
-      plotData$: this.hazardPlotData$,
+      diffPlotData: this.hazardDiffPlotData,
+      plotData: this.hazardPlotData,
       title: 'Hazard Curves',
     },
     {
-      diffPlotData$: this.spectraDiffPlotData$,
-      plotData$: this.spectraPlotData$,
+      diffPlotData: this.spectraDiffPlotData,
+      plotData: this.spectraPlotData,
       title: 'Spectra',
     },
     {
-      plotData$: this.hazardComponentsPlotData$,
+      plotData: this.hazardComponentsPlotData,
       title: 'Hazard Components',
     },
     {
-      plotData$: this.spectraComponentsPlotData$,
+      plotData: this.spectraComponentsPlotData,
       title: 'Spectra Components',
     },
   ];
 
   /** Repo metadata */
-  repositories$ = this.facade.usage$.pipe(
-    map(usage => usage?.metadata.repositories)
-  );
+  repositories = computed(() => this.facade.usage()?.metadata.repositories);
 
   constructor(
     private facade: AppFacade,
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html
index dad4df7af0054e45762c5aac955114a5f4dbfadd..4926ad259f303a5802d370cf6228d361e9e4f444 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.html
@@ -1,20 +1,20 @@
 <div class="grid-container-widescreen-xl margin-y-3">
   <mat-accordion multi>
     <app-table-data-panel
-      [filename]="spectraFilename$ | async"
-      [tableData]="spectraTotalTableData$ | async"
+      [filename]="spectraFilename"
+      [tableData]="spectraTotalTableData()"
       panelTitle="Response Spectra: Total"
     />
 
     <app-table-data-panel
-      [filename]="spectraDiffFilename$ | async"
-      [tableData]="spectraDiffTableData$ | async"
+      [filename]="spectraDiffFilename"
+      [tableData]="spectraDiffTableData()"
       panelTitle="Response Spectra: Percent Difference"
     />
 
     <app-table-data-panel
-      [filename]="spectraComponentsFilename$ | async"
-      [tableData]="spectraComponentsTableData$ | async"
+      [filename]="spectraComponentsFilename"
+      [tableData]="spectraComponentsTableData()"
       panelTitle="Response Spectra: Components"
     />
   </mat-accordion>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.spec.ts
index 03edf5f3627fa03199db0bec9e2b59b424959f42..5206c02f20730b21c94e1a5c77da2ba58785472d 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.spec.ts
@@ -1,7 +1,7 @@
 import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
-import {provideMockStore} from '@ngrx/store/testing';
+import {provideRouter} from '@angular/router';
 
 import {SpectraDataComponent} from './spectra-data.component';
 
@@ -13,7 +13,7 @@ describe('SpectraDataComponent', () => {
     await TestBed.configureTestingModule({
       imports: [SpectraDataComponent],
       providers: [
-        provideMockStore({initialState: {}}),
+        provideRouter([]),
         provideHttpClient(),
         provideNoopAnimations(),
       ],
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts
index c9192d6e93940b686dccf4468cd163b65989483e..12bb257303f592946db97e4ecd85662c51f6d026 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/spectra-data/spectra-data.component.ts
@@ -1,14 +1,12 @@
 import {AsyncPipe} from '@angular/common';
-import {Component} from '@angular/core';
+import {Component, computed, Signal} from '@angular/core';
 import {MatAccordion} from '@angular/material/expansion';
-import {TableData} from '@ghsc/nshmp-lib-ng/nshmp';
+import {TableData} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 import {
   SourceType,
   sourceTypeToCapitalCase,
 } from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import equal from 'fast-deep-equal';
-import {combineLatest, map, Observable} from 'rxjs';
 
 import {AppFacade} from '../../state/app.facade';
 import {
@@ -62,98 +60,91 @@ interface ToTableDataOptions {
 })
 export class SpectraDataComponent {
   /** Model values */
-  private models$ = this.facade.controlPanelForm$.pipe(
-    map(form => `${form.value.model}-${form.value.modelCompare}`)
-  );
+  private models = `${this.facade.formGroup.getRawValue().model}-${this.facade.formGroup.getRawValue().modelCompare}`;
 
   /** Filename for hazard components export CSV */
-  spectraComponentsFilename$ = this.models$.pipe(
-    map(models => `spectra-components-${models}.csv`)
-  );
+  spectraComponentsFilename = `spectra-components-${this.models}.csv`;
 
   /** Filename for hazard difference export CSV */
-  spectraDiffFilename$ = this.models$.pipe(
-    map(models => `spectra-%-diff-${models}.csv`)
-  );
+  spectraDiffFilename = `spectra-%-diff-${this.models}.csv`;
 
   /** Filename for hazard export CSV */
-  spectraFilename$ = this.models$.pipe(
-    map(models => `spectra-compare-${models}.csv`)
-  );
+  spectraFilename = `spectra-compare-${this.models}.csv`;
 
   /** Table data for response spectra components */
-  spectraComponentsTableData$ = combineLatest([
-    this.facade.serviceResponses$,
-    this.facade.controlPanelForm$,
-    this.facade.availableModels$,
-  ]).pipe(
-    map(([serviceResponses, form, availableModels]) => {
-      const imts = this.spectraImtRow(serviceResponses);
-
-      const spectraFilter = (spectra: Spectra) =>
-        spectra.sourceType !== SourceType.TOTAL;
-
-      return [
-        imts,
-        ...this.serviceResponsesToTableData({
-          availableModels,
-          name: (modelInfo: Parameter, spectra: Spectra) =>
-            `${modelInfo.display} - ${sourceTypeToCapitalCase(
-              spectra.sourceType
-            )}`,
-          returnPeriod: form.value.returnPeriod,
-          serviceResponses,
-          spectraFilter,
-        }),
-      ];
-    })
-  );
+  spectraComponentsTableData = computed(() => {
+    const serviceResponses = this.facade.serviceResponses();
+
+    if (serviceResponses.modelA === null || serviceResponses.modelB === null) {
+      return [];
+    }
+
+    const imts = this.spectraImtRow(serviceResponses);
+
+    const spectraFilter = (spectra: Spectra) =>
+      spectra.sourceType !== SourceType.TOTAL;
+
+    return [
+      imts,
+      ...this.serviceResponsesToTableData({
+        availableModels: this.facade.availableModels(),
+        name: (modelInfo: Parameter, spectra: Spectra) =>
+          `${modelInfo.display} - ${sourceTypeToCapitalCase(
+            spectra.sourceType
+          )}`,
+        returnPeriod: this.facade.formGroup.getRawValue().returnPeriod,
+        serviceResponses: this.facade.serviceResponses(),
+        spectraFilter,
+      }),
+    ];
+  });
 
   /** Table data for response spectra difference */
-  spectraDiffTableData$: Observable<TableData[]> = combineLatest([
-    this.facade.serviceResponses$,
-    this.facade.controlPanelForm$,
-  ]).pipe(
-    map(([serviceResponses, form]) => {
-      const spectraDiff = spectraPercentDifference(
-        serviceResponses,
-        form.value.returnPeriod
-      );
-
-      return [
-        this.spectraImtRow(serviceResponses),
-        {
-          td: spectraDiff.percentDifference,
-          th: 'Percent Difference',
-        },
-      ];
-    })
-  );
+  spectraDiffTableData: Signal<TableData[]> = computed(() => {
+    const serviceResponses = this.facade.serviceResponses();
+
+    if (serviceResponses.modelA === null || serviceResponses.modelB === null) {
+      return [];
+    }
+
+    const spectraDiff = spectraPercentDifference(
+      serviceResponses,
+      this.facade.formGroup.getRawValue().returnPeriod
+    );
+
+    return [
+      this.spectraImtRow(serviceResponses),
+      {
+        td: spectraDiff.percentDifference,
+        th: 'Percent Difference',
+      },
+    ];
+  });
 
   /** Table data for response spectra */
-  spectraTotalTableData$ = combineLatest([
-    this.facade.serviceResponses$,
-    this.facade.controlPanelForm$,
-    this.facade.availableModels$,
-  ]).pipe(
-    map(([serviceResponses, form, availableModels]) => {
-      const imts = this.spectraImtRow(serviceResponses);
-
-      const spectraFilter = (spectra: Spectra) =>
-        spectra.sourceType === SourceType.TOTAL;
-
-      return [
-        imts,
-        ...this.serviceResponsesToTableData({
-          availableModels,
-          name: (modelInfo: Parameter) => modelInfo.display,
-          returnPeriod: form.value.returnPeriod,
-          serviceResponses,
-          spectraFilter,
-        }),
-      ];
-    })
-  );
+  spectraTotalTableData = computed(() => {
+    const serviceResponses = this.facade.serviceResponses();
+
+    if (serviceResponses.modelA === null || serviceResponses.modelB === null) {
+      return [];
+    }
+
+    const imts = this.spectraImtRow(serviceResponses);
+
+    const spectraFilter = (spectra: Spectra) =>
+      spectra.sourceType === SourceType.TOTAL;
+
+    return [
+      imts,
+      ...this.serviceResponsesToTableData({
+        availableModels: this.facade.availableModels(),
+        name: (modelInfo: Parameter) => modelInfo.display,
+        returnPeriod: this.facade.formGroup.getRawValue().returnPeriod,
+        serviceResponses,
+        spectraFilter,
+      }),
+    ];
+  });
 
   constructor(private facade: AppFacade) {}
 
@@ -170,12 +161,11 @@ export class SpectraDataComponent {
       spectra => spectra.sourceType === SourceType.TOTAL
     );
 
-    if (!equal(spectraA.responseSpectra.imts, spectraB.responseSpectra.imts)) {
-      throw new Error('Response spectra IMTs do not match');
-    }
+    const imtsA = spectraA.responseSpectra.imts;
+    const imtsB = spectraB.responseSpectra.imts;
 
     return {
-      td: spectraA.responseSpectra.imts,
+      td: imtsA.length >= imtsB.length ? imtsA : imtsB,
       th: 'IMT',
     };
   }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html
index b0b49ca7b35221d3a701444f50dcc4ce4e9a6846..d83e9cb1bac3cec10df3c384b1a32e174e51bc8f 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.html
@@ -3,11 +3,11 @@
     <mat-panel-title>{{ panelTitle }}</mat-panel-title>
   </mat-expansion-panel-header>
 
-  <nshmp-lib-ng-export-data-table
+  <nshmp-lib-no-ngrx-export-data-table
     [table]="tableData"
     [filename]="filename | lowercase"
     [buttonText]="buttonText"
   />
 
-  <nshmp-lib-ng-data-table [table]="tableData" />
+  <nshmp-lib-no-ngrx-data-table [table]="tableData" />
 </mat-expansion-panel>
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.spec.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.spec.ts
index 9f3bb55fb1641af9fca5736f4a189e397681f4c7..e55d98b55c4d156431cf20c42e74fb3e452327dc 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.spec.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.spec.ts
@@ -1,6 +1,7 @@
 import {provideHttpClient} from '@angular/common/http';
 import {ComponentFixture, TestBed} from '@angular/core/testing';
 import {provideNoopAnimations} from '@angular/platform-browser/animations';
+import {provideRouter} from '@angular/router';
 
 import {TableDataPanelComponent} from './table-data-panel.component';
 
@@ -11,7 +12,11 @@ describe('TableDataPanelComponent', () => {
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       imports: [TableDataPanelComponent],
-      providers: [provideHttpClient(), provideNoopAnimations()],
+      providers: [
+        provideHttpClient(),
+        provideNoopAnimations(),
+        provideRouter([]),
+      ],
     }).compileComponents();
 
     fixture = TestBed.createComponent(TableDataPanelComponent);
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts
index 5a61dc77338d6e12ed34196a9a063ca33c9dd63e..43861892039a1f488c4c277018d0eb3a59611240 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/components/table-data-panel/table-data-panel.component.ts
@@ -9,7 +9,7 @@ import {
   NshmpLibNgDataTableComponent,
   NshmpLibNgExportDataTableComponent,
   TableData,
-} from '@ghsc/nshmp-lib-ng/nshmp';
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
 
 /**
  * Expansion panel with data table and export.
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.actions.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.actions.ts
deleted file mode 100644
index 5a060e853227a678d96131ff6800f7aef2685b73..0000000000000000000000000000000000000000
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.actions.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import {ResponseSpectra} from '@ghsc/nshmp-lib-ng/hazard';
-import {HazardUsageResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
-import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
-import {Location} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/geo';
-import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {createActionGroup, emptyProps, props} from '@ngrx/store';
-import {sharedActions} from 'projects/nshmp-apps/src/shared/state/shared';
-
-import {ServiceResponses} from './app.state';
-
-/**
- * Application NGRX actions
- */
-export const appActions = createActionGroup({
-  events: {
-    ...sharedActions,
-    /** Action for available NSHMs */
-    'Available Models': props<{models: Parameter[]}>(),
-    /** Action to call services from query is valid */
-    'Initial Call From Query': emptyProps(),
-    /** Action to set the initial form values from query or default */
-    'Initial Form Set': emptyProps(),
-    /** Action for NSHM metadata */
-    'nshm Services': props<{nshmServices: NshmMetadata[]}>(),
-    /** Set the response spectra data action */
-    'Response Spectra': props<{responseSpectra: ResponseSpectra[]}>(),
-    /** Set the service response action */
-    'Service Responses': props<{serviceResponses: ServiceResponses}>(),
-    /** Set the site location action */
-    'Set Location': props<{location: Location}>(),
-    /** Set the usage responses action */
-    'Usage Responses': props<{
-      usageResponses: Map<string, HazardUsageResponse>;
-    }>(),
-  },
-  source: 'Development Dynamic Hazard Compare',
-});
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.effects.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.effects.ts
deleted file mode 100644
index 8f4c7f8f60759a76a17a8c3fcaaa9783f6afba6b..0000000000000000000000000000000000000000
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.effects.ts
+++ /dev/null
@@ -1,474 +0,0 @@
-import {Injectable} from '@angular/core';
-import {ActivatedRoute, Router} from '@angular/router';
-import {
-  HazardFormControlIds,
-  HazardService,
-  hazardUtils,
-} from '@ghsc/nshmp-lib-ng/hazard';
-import {
-  NshmpService,
-  ServiceCallInfo,
-  SpinnerService,
-} from '@ghsc/nshmp-lib-ng/nshmp';
-import {
-  HazardCalcResponse,
-  HazardRequestMetadata,
-  HazardResponse,
-} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
-import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
-import {Imt, NehrpSiteClass} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
-import {sourceTypeFromPascalCase} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
-import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
-import {Actions, createEffect, ofType} from '@ngrx/effects';
-import {concatLatestFrom} from '@ngrx/operators';
-import {Store} from '@ngrx/store';
-import {MarkAsDirtyAction, SetValueAction} from 'ngrx-forms';
-import {environment} from 'projects/nshmp-apps/src/environments/environment';
-import {devApps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
-import {catchError, exhaustMap, forkJoin, map, mergeMap, tap} from 'rxjs';
-
-import {createPlots} from '../utils/response-handler.utils';
-import {appActions} from './app.actions';
-import {dynamicCompareAppFeature} from './app.reducer';
-import {
-  ControlForm,
-  controlPanelFormInitialState,
-  FORM_ID,
-  ServiceResponses,
-  Spectra,
-} from './app.state';
-
-/**
- * URL query.
- */
-interface Query {
-  imt: string;
-  latitude: string;
-  longitude: string;
-  maxDirection: string;
-  model: string;
-  modelCompare: string;
-  returnPeriod: string;
-  siteClass: string;
-  truncate: string;
-  vs30: string;
-}
-
-/**
- * NGRX effects for dynamic hazard compare application.
- */
-@Injectable()
-export class DynamicCompareAppEffects {
-  /** nshmp-haz-ws web config */
-  nshmpHazWs = environment.webServices.nshmpHazWs;
-  /** Hazard endpoint */
-  serviceEndpoint = this.nshmpHazWs.services.curveServices.hazard;
-
-  /**
-   * Create the plot data from the service response.
-   */
-  createPlots$ = createEffect(() =>
-    this.actions$.pipe(
-      ofType(appActions.serviceResponses),
-      concatLatestFrom(() =>
-        this.store.select(dynamicCompareAppFeature.selectDynamicCompareAppState)
-      ),
-      map(([, state]) => {
-        const plots = createPlots(state);
-        this.spinnerService.remove();
-        return appActions.plots({plots});
-      }),
-      catchError((error: Error) => this.nshmpService.throwError$(error))
-    )
-  );
-
-  /**
-   * Call dynamic hazard services.
-   */
-  callServices$ = createEffect(() =>
-    this.actions$.pipe(
-      ofType(appActions.callServices),
-      concatLatestFrom(() =>
-        this.store.select(dynamicCompareAppFeature.selectDynamicCompareAppState)
-      ),
-      exhaustMap(([, state]) => {
-        this.spinnerService.show(SpinnerService.MESSAGE_SERVICE);
-        const form = state.controlPanelForm.value;
-
-        const modelUrl = this.serviceCallUrl(
-          form.model,
-          form,
-          state.nshmServices
-        );
-
-        const modelCompareUrl = this.serviceCallUrl(
-          form.modelCompare,
-          form,
-          state.nshmServices
-        );
-
-        const serviceCallInfo: ServiceCallInfo = {
-          ...state.serviceCallInfo,
-          serviceCalls: [modelUrl, modelCompareUrl],
-        };
-
-        const calls = [modelUrl, modelCompareUrl].map(url =>
-          this.nshmpService.callService$(url)
-        );
-
-        return forkJoin(calls).pipe(
-          mergeMap((hazardResponses: HazardCalcResponse[]) => {
-            if (hazardResponses.length !== 2) {
-              throw new Error('Does not contain two responses');
-            }
-
-            const modelA = hazardResponses[0];
-            const modelB = hazardResponses[1];
-
-            const serviceResponses: ServiceResponses = {
-              modelA: {
-                hazardResponse: modelA,
-                model: form.model,
-                spectra: this.responseSpectra(
-                  modelA.response.hazardCurves,
-                  form
-                ),
-              },
-              modelB: {
-                hazardResponse: modelB,
-                model: form.modelCompare,
-                spectra: this.responseSpectra(
-                  modelB.response.hazardCurves,
-                  form
-                ),
-              },
-            };
-
-            return [
-              appActions.serviceResponses({serviceResponses}),
-              appActions.serviceCallInfo({serviceCallInfo}),
-            ];
-          }),
-          catchError((error: Error) => this.nshmpService.throwError$(error))
-        );
-      }),
-      catchError((error: Error) => this.nshmpService.throwError$(error))
-    )
-  );
-
-  /**
-   * Initialize the app by getting all the usages from the available dynamic services.
-   */
-  init$ = createEffect(() =>
-    this.actions$.pipe(
-      ofType(appActions.init),
-      concatLatestFrom(() =>
-        this.store.select(dynamicCompareAppFeature.selectDynamicCompareAppState)
-      ),
-      exhaustMap(([, state]) => {
-        this.spinnerService.show(SpinnerService.MESSAGE_METADATA);
-
-        return this.hazardService
-          .dynamicNshms$<HazardRequestMetadata>(
-            `${this.nshmpHazWs.url}${this.nshmpHazWs.services.nshms}`,
-            this.serviceEndpoint
-          )
-          .pipe(
-            mergeMap(({models, nshmServices, usageResponses}) => {
-              this.spinnerService.remove();
-              const serviceCallInfo: ServiceCallInfo = {
-                ...state.serviceCallInfo,
-                usage: nshmServices.map(
-                  service => `${service.url}${this.serviceEndpoint}`
-                ),
-              };
-
-              return [
-                appActions.nshmServices({nshmServices}),
-                appActions.usageResponses({usageResponses}),
-                appActions.serviceCallInfo({serviceCallInfo}),
-                appActions.availableModels({models}),
-                appActions.initialFormSet(),
-              ];
-            }),
-            catchError((error: Error) => this.nshmpService.throwError$(error))
-          );
-      }),
-      catchError((error: Error) => this.nshmpService.throwError$(error))
-    )
-  );
-
-  /**
-   * Throw error if no comparable models are found.
-   */
-  noComparableModel$ = createEffect(
-    () =>
-      this.actions$.pipe(
-        ofType(SetValueAction.TYPE),
-        concatLatestFrom(() =>
-          this.store.select(dynamicCompareAppFeature.selectControlPanelForm)
-        ),
-        tap(([action, {value}]) => {
-          const setValueAction = action as SetValueAction<unknown>;
-
-          if (
-            setValueAction.controlId ===
-              `${FORM_ID}.${HazardFormControlIds.MODEL}` &&
-            value.modelCompare === null
-          ) {
-            this.nshmpService.throwError$(
-              new Error(
-                'No available models to compare.<br>Select a different starting model.'
-              )
-            );
-          }
-        }),
-        catchError((error: Error) => this.nshmpService.throwError$(error))
-      ),
-    {
-      dispatch: false,
-    }
-  );
-
-  /**
-   * Check if form is valid from the url query parameters and call service.
-   */
-  initialCallFromUrlQuery$ = createEffect(() =>
-    this.actions$.pipe(
-      ofType(appActions.initialCallFromQuery),
-      concatLatestFrom(() =>
-        this.store.select(dynamicCompareAppFeature.selectControlPanelForm)
-      ),
-      mergeMap(([, form]) => {
-        if (form.isValid) {
-          this.nshmpService.selectPlotControl();
-          return [new MarkAsDirtyAction(form.id), appActions.callServices()];
-        } else if (form.value !== controlPanelFormInitialState().value) {
-          return [new MarkAsDirtyAction(form.id)];
-        } else {
-          return [];
-        }
-      }),
-      catchError((error: Error) => this.nshmpService.throwError$(error))
-    )
-  );
-
-  /**
-   * Set the form values.
-   */
-  initialFormSet$ = createEffect(() =>
-    this.actions$.pipe(
-      ofType(appActions.initialFormSet),
-      concatLatestFrom(() =>
-        this.store.select(dynamicCompareAppFeature.selectControlPanelForm)
-      ),
-      mergeMap(([, form]) => {
-        const query = this.route.snapshot.queryParams as Query;
-        const defaultValues = controlPanelFormInitialState().value;
-
-        const formValues: ControlForm = {
-          commonReturnPeriods: defaultValues.commonReturnPeriods,
-          imt: this.getImt(query.imt, defaultValues.imt),
-          latitude: query.latitude
-            ? Number.parseFloat(query.latitude)
-            : defaultValues.latitude,
-          longitude: query.longitude
-            ? Number.parseFloat(query.longitude)
-            : defaultValues.longitude,
-          maxDirection:
-            query.maxDirection !== undefined
-              ? (JSON.parse(query.maxDirection) as boolean)
-              : defaultValues.maxDirection,
-          model: this.getModel(query.model, defaultValues.model),
-          modelCompare: this.getModel(
-            query.modelCompare,
-            defaultValues.modelCompare
-          ),
-          returnPeriod: query.returnPeriod
-            ? Number.parseInt(query.returnPeriod, 10)
-            : defaultValues.returnPeriod,
-          siteClass: this.getSiteClass(
-            query.siteClass,
-            defaultValues.siteClass as NehrpSiteClass
-          ),
-          sourceType: null,
-          truncate:
-            query.truncate !== undefined
-              ? (JSON.parse(query.truncate) as boolean)
-              : defaultValues.truncate,
-          vs30: query.vs30 ? Number.parseFloat(query.vs30) : defaultValues.vs30,
-        };
-
-        const actions = hazardUtils.initialFormSetActions(form, formValues);
-        actions.push(
-          new SetValueAction(
-            form.controls.modelCompare.id,
-            formValues.modelCompare
-          )
-        );
-        actions.push(new SetValueAction(form.controls.imt.id, formValues.imt));
-        actions.push(
-          new SetValueAction(form.controls.vs30.id, formValues.vs30)
-        );
-        actions.push(appActions.initialCallFromQuery());
-        return actions;
-      }),
-      catchError((error: Error) => this.nshmpService.throwError$(error))
-    )
-  );
-
-  /**
-   * Effect to set location form fields.
-   */
-  setLocation$ = createEffect(() =>
-    this.actions$.pipe(
-      ofType(appActions.setLocation),
-      concatLatestFrom(() =>
-        this.store.select(dynamicCompareAppFeature.selectControlPanelForm)
-      ),
-      exhaustMap(([{location}, form]) => {
-        return [
-          new SetValueAction(form.controls.latitude.id, location.latitude),
-          new MarkAsDirtyAction(form.controls.latitude.id),
-          new SetValueAction(form.controls.longitude.id, location.longitude),
-          new MarkAsDirtyAction(form.controls.longitude.id),
-        ];
-      }),
-      catchError((error: Error) => this.nshmpService.throwError$(error))
-    )
-  );
-
-  /**
-   * Update the URL query for settings
-   */
-  updateUrl$ = createEffect(
-    () =>
-      this.actions$.pipe(
-        ofType(SetValueAction.TYPE),
-        concatLatestFrom(() =>
-          this.store.select(dynamicCompareAppFeature.selectControlPanelForm)
-        ),
-        map(([action, {value}]) => {
-          if ((action as SetValueAction<unknown>).controlId.includes(FORM_ID)) {
-            const query: Query = {
-              imt: value.imt,
-              latitude: value.latitude?.toString(),
-              longitude: value.longitude?.toString(),
-              maxDirection: String(value.maxDirection),
-              model: value.model,
-              modelCompare: value.modelCompare,
-              returnPeriod: value.returnPeriod?.toString(),
-              siteClass: value.siteClass,
-              truncate: String(value.truncate),
-              vs30: value.vs30?.toString(),
-            };
-
-            this.router
-              .navigate([devApps().hazard.dynamicCompare.routerLink], {
-                queryParams: {
-                  ...query,
-                },
-              })
-              .catch((error: Error) => this.nshmpService.throwError$(error));
-          }
-        }),
-        catchError((error: Error) => this.nshmpService.throwError$(error))
-      ),
-    {dispatch: false}
-  );
-
-  constructor(
-    private actions$: Actions,
-    private hazardService: HazardService,
-    private nshmpService: NshmpService,
-    private route: ActivatedRoute,
-    private router: Router,
-    private spinnerService: SpinnerService,
-    private store: Store
-  ) {}
-
-  /**
-   * Returns the IMT to use.
-   *
-   * @param query The query value
-   * @param defaultImt The default IMT
-   */
-  private getImt(query: string, defaultImt: Imt): Imt {
-    return (
-      Object.values(Imt).find(imt => imt.toString() === query) ?? defaultImt
-    );
-  }
-
-  /**
-   * Returns the model to use.
-   *
-   * @param query The query model
-   * @param defaultModel The default model
-   */
-  private getModel(query: string, defaultModel: NshmId): NshmId {
-    return (
-      Object.values(NshmId).find(model => model.toString() === query) ??
-      defaultModel
-    );
-  }
-
-  /**
-   * Returns the site class to use.
-   *
-   * @param query The query value
-   * @param defaultSiteClass The default site class
-   */
-  private getSiteClass(
-    query: string,
-    defaultSiteClass: NehrpSiteClass
-  ): NehrpSiteClass {
-    return (
-      Object.values(NehrpSiteClass).find(
-        siteClass => siteClass.toString() === query
-      ) ?? defaultSiteClass
-    );
-  }
-
-  /**
-   * Create the response spectra for a specific model and for each source type available.
-   *
-   * @param hazardResponses The hazard responses
-   * @param form The control form values
-   */
-  private responseSpectra(
-    hazardResponses: HazardResponse[],
-    form: ControlForm
-  ): Spectra[] {
-    const spectra: Spectra[] = [];
-    const sourceTypes = [...hazardResponses]
-      .shift()
-      .data.map(data => data.component);
-
-    sourceTypes.forEach(sourceType => {
-      form.sourceType = sourceType;
-      spectra.push({
-        responseSpectra: hazardUtils.responseSpectra(hazardResponses, form),
-        sourceType: sourceTypeFromPascalCase(sourceType),
-      });
-    });
-
-    return spectra;
-  }
-
-  /**
-   * Returns the URL to call for a specific model.
-   *
-   * @param model The NSHM to call
-   * @param formValues The control panel form values
-   * @param nshmServices The NSHM service metadata
-   */
-  private serviceCallUrl(
-    model: NshmId,
-    formValues: ControlForm,
-    nshmServices: NshmMetadata[]
-  ): string {
-    const {latitude, longitude, vs30} = formValues;
-    const service = nshmServices.find(service => service.model === model);
-    return `${service.url}${this.serviceEndpoint}/${longitude}/${latitude}/${vs30}`;
-  }
-}
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts
index 74e1ff942692b5d4c774ede23a40a78f4142f28e..f17c639334f0a75c5630a20e35cf23ee725c898e 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.facade.ts
@@ -1,18 +1,61 @@
-import {Injectable} from '@angular/core';
-import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp';
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
-import {HazardUsageResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
+import {computed, Injectable, Signal, signal} from '@angular/core';
+import {AbstractControl, FormBuilder, Validators} from '@angular/forms';
+import {ActivatedRoute, Router} from '@angular/router';
+import {HazardService, hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {
+  NshmpService,
+  ServiceCallInfo,
+  SpinnerService,
+} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
+import {
+  HazardCalcResponse,
+  HazardRequestMetadata,
+  HazardResponse,
+  HazardUsageResponse,
+} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
 import {NshmMetadata} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/nshm-service';
 import {Location} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/geo';
+import {Imt, NehrpSiteClass} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
+import {sourceTypeFromPascalCase} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
+import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {select, Store} from '@ngrx/store';
-import {FormGroupState} from 'ngrx-forms';
 import {environment} from 'projects/nshmp-apps/src/environments/environment';
-import {Observable} from 'rxjs';
+import {devApps} from 'projects/nshmp-apps/src/shared/utils/applications.utils';
+import {
+  redrawPlots,
+  resetPlotSettings,
+  validateNan,
+} from 'projects/nshmp-apps/src/shared/utils/facade.utils';
+import {catchError, forkJoin, Observable} from 'rxjs';
 
-import {appActions} from './app.actions';
-import {dynamicCompareAppFeature} from './app.reducer';
-import {ControlForm, ServiceResponses} from './app.state';
+import {defaultPlots} from '../utils/app.utils';
+import {createPlots} from '../utils/response-handler.utils';
+import * as responseHandler from '../utils/response-handler.utils';
+import {
+  AppState,
+  ControlForm,
+  controlPanelFormInitialState,
+  initialState,
+  ServiceResponses,
+  Spectra,
+} from './app.state';
+
+/**
+ * URL query.
+ */
+interface Query {
+  imt: string;
+  latitude: string;
+  longitude: string;
+  maxDirection: string;
+  model: string;
+  modelCompare: string;
+  returnPeriod: string;
+  siteClass: string;
+  truncate: string;
+  vs30: string;
+}
 
 /**
  * Entrypoint to NGRX store for dynamic hazard compare application.
@@ -21,128 +64,265 @@ import {ControlForm, ServiceResponses} from './app.state';
   providedIn: 'root',
 })
 export class AppFacade {
+  public readonly formGroup = this.formBuilder.group<ControlForm>(
+    controlPanelFormInitialState()
+  );
+  public readonly state = signal<AppState>(initialState());
+
   /** nshmp-haz-ws web config */
-  nshmpHazWs = environment.webServices.nshmpHazWs;
+  public nshmpHazWs = environment.webServices.nshmpHazWs;
   /** Hazard endpoint */
-  serviceEndpoint = this.nshmpHazWs.services.curveServices.hazard;
+  public serviceEndpoint = this.nshmpHazWs.services.curveServices.hazard;
 
-  constructor(private store: Store) {}
+  constructor(
+    private formBuilder: FormBuilder,
+    private spinnerService: SpinnerService,
+    private nshmpService: NshmpService,
+    private hazardService: HazardService,
+    private route: ActivatedRoute,
+    private router: Router
+  ) {
+    this.addValidators();
+  }
 
   /**
    * Returns the available dynamic models.
    */
-  get availableModels$(): Observable<Parameter[]> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectAvailableModels)
-    );
+  public get availableModels(): Signal<Parameter[]> {
+    return computed(() => this.state().availableModels);
   }
 
   /**
    * Returns the models that can been compared with selected model.
    */
-  get comparableModels$(): Observable<Parameter[]> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectComparableModels)
-    );
+  public get comparableModels(): Signal<Parameter[]> {
+    return computed(() => this.state().comparableModels);
   }
 
   /**
-   * Returns the control panel form state.
+   * Returns the metadata of the NSHM observable.
    */
-  get controlPanelForm$(): Observable<FormGroupState<ControlForm>> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectControlPanelForm)
+  public get nshmService(): Signal<NshmMetadata> {
+    return computed(() =>
+      this.state().nshmServices.find(
+        nshmService => nshmService.model === this.formGroup.getRawValue().model
+      )
     );
   }
 
   /**
    * Returns the `Map` of the plots.
    */
-  get plots$(): Observable<Map<string, NshmpPlot>> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectPlots));
-  }
-
-  /**
-   * Returns the metadata of the NSHM observable.
-   */
-  get nshmService$(): Observable<NshmMetadata> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectNshmService));
+  public get plots(): Signal<Map<string, NshmpPlot>> {
+    return computed(() => this.state().plots);
   }
 
   /**
    * Returns the service call info.
    */
-  get serviceCallInfo$(): Observable<ServiceCallInfo> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectServiceCallInfo)
-    );
+  public get serviceCallInfo(): Signal<ServiceCallInfo> {
+    return computed(() => this.state().serviceCallInfo);
   }
 
   /**
    * Returns the service responses.
    */
-  get serviceResponses$(): Observable<ServiceResponses> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectServiceResponses)
-    );
+  public get serviceResponses(): Signal<ServiceResponses> {
+    return computed(() => this.state().serviceResponses);
   }
 
   /**
    * Return the usage for the selected model.
    */
-  get usage$(): Observable<HazardUsageResponse> {
-    return this.store.pipe(
-      select(dynamicCompareAppFeature.selectCombinedUsage)
-    );
+  public get usage(): Signal<HazardUsageResponse> {
+    return computed(() => this.state().combinedUsage);
   }
 
   /**
    * Return the usage for the selected model.
    */
-  get usageModelA$(): Observable<HazardUsageResponse> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectUsageModelA));
+  public get usageModelA(): Signal<HazardUsageResponse> {
+    return computed(() =>
+      this.state().usageResponses?.get(this.formGroup.getRawValue().model)
+    );
   }
 
   /**
    * Return the usage for the selected model.
    */
-  get usageModelB$(): Observable<HazardUsageResponse> {
-    return this.store.pipe(select(dynamicCompareAppFeature.selectUsageModelB));
+  public get usageModelB(): Signal<HazardUsageResponse> {
+    return computed(() =>
+      this.state().usageResponses?.get(
+        this.formGroup.getRawValue().modelCompare
+      )
+    );
   }
 
   /**
    * Call the hazard service.
    */
-  callServices(): void {
-    this.store.dispatch(appActions.callServices());
+  public callServices(): void {
+    this.spinnerService.show(SpinnerService.MESSAGE_SERVICE);
+    const form = this.formGroup.getRawValue();
+
+    const modelUrl = this.serviceCallUrl(
+      form.model,
+      form,
+      this.state().nshmServices
+    );
+
+    const modelCompareUrl = this.serviceCallUrl(
+      form.modelCompare,
+      form,
+      this.state().nshmServices
+    );
+
+    const serviceCallInfo: ServiceCallInfo = {
+      ...this.state().serviceCallInfo,
+      serviceCalls: [modelUrl, modelCompareUrl],
+    };
+
+    const calls: Observable<HazardCalcResponse>[] = [
+      modelUrl,
+      modelCompareUrl,
+    ].map(url => this.nshmpService.callService$(url));
+
+    forkJoin(calls)
+      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
+      .subscribe(hazardResponses => {
+        if (hazardResponses.length !== 2) {
+          throw new Error('Does not contain two responses');
+        }
+
+        const modelA = hazardResponses[0];
+        const modelB = hazardResponses[1];
+
+        const serviceResponses: ServiceResponses = {
+          modelA: {
+            hazardResponse: modelA,
+            model: form.model,
+            spectra: this.responseSpectra(modelA.response.hazardCurves, form),
+          },
+          modelB: {
+            hazardResponse: modelB,
+            model: form.modelCompare,
+            spectra: this.responseSpectra(modelB.response.hazardCurves, form),
+          },
+        };
+
+        this.updateState({serviceCallInfo, serviceResponses});
+        this.createPlots();
+        this.spinnerService.remove();
+      });
+  }
+
+  public createPlots(): void {
+    const plots = createPlots(this.state(), this.formGroup);
+    this.updateState({plots});
   }
 
   /**
    * Initialize applicaiton.
    */
-  init(): void {
-    this.store.dispatch(appActions.init());
+  public init(): void {
+    this.spinnerService.show(SpinnerService.MESSAGE_METADATA);
+
+    this.hazardService
+      .dynamicNshms$<HazardRequestMetadata>(
+        `${this.nshmpHazWs.url}${this.nshmpHazWs.services.nshms}`,
+        this.serviceEndpoint
+      )
+      .pipe(catchError((error: Error) => this.nshmpService.throwError$(error)))
+      .subscribe(({models, nshmServices, usageResponses}) => {
+        this.spinnerService.remove();
+        const serviceCallInfo: ServiceCallInfo = {
+          ...this.state().serviceCallInfo,
+          usage: nshmServices.map(
+            service => `${service.url}${this.serviceEndpoint}`
+          ),
+        };
+
+        this.updateState({
+          availableModels: models,
+          nshmServices,
+          serviceCallInfo,
+          usageResponses,
+        });
+
+        this.onModelChange();
+        this.initialFormSet();
+      });
+  }
+
+  /**
+   * Returns the updated state for a model change.
+   *
+   * @param state The current application state
+   */
+  public onModelChange(): void {
+    const comparableModels = this.getComparableModels();
+    const defaultComparableModel = [...comparableModels]?.pop()?.value ?? null;
+    const combinedUsage = this.getCombinedUsage(
+      this.state().usageResponses,
+      this.formGroup.getRawValue().model,
+      defaultComparableModel
+    );
+
+    if (
+      this.formGroup.getRawValue().modelCompare !==
+      (defaultComparableModel as NshmId)
+    ) {
+      this.formGroup.patchValue({
+        modelCompare: defaultComparableModel as NshmId,
+      });
+    }
+
+    this.updateState({
+      combinedUsage,
+      comparableModels,
+      plots: initialState().plots,
+      serviceCallInfo: {
+        ...this.state().serviceCallInfo,
+        serviceCalls: [],
+      },
+      serviceResponses: initialState().serviceResponses,
+    });
   }
 
   /**
    * Redraw the plots.
    */
-  plotRedraw(): void {
-    this.store.dispatch(appActions.plotRedraw());
+  public plotRedraw(): void {
+    redrawPlots(this.state().plots);
   }
 
   /**
    * Reset the control panel.
    */
-  resetControlPanel(): void {
-    this.store.dispatch(appActions.resetControlPanel());
+  public resetControlPanel(): void {
+    this.formGroup.reset(controlPanelFormInitialState());
+    this.resetState();
+    this.onModelChange();
   }
 
   /**
    * Reset the plot settings.
    */
-  resetSettings(): void {
-    this.store.dispatch(appActions.resetSettings());
+  public resetSettings(): void {
+    resetPlotSettings({
+      currentPlots: this.state().plots,
+      defaultPlots: defaultPlots(),
+    });
+  }
+
+  public resetState(): void {
+    this.updateState({
+      plots: initialState().plots,
+      serviceCallInfo: {
+        ...this.state().serviceCallInfo,
+        serviceCalls: [],
+      },
+    });
   }
 
   /**
@@ -152,7 +332,7 @@ export class AppFacade {
    * @param nshmServices The NSHM service metadata
    * @param serviceEndpoint The service endpoint to call
    */
-  serviceCallUrls(
+  public serviceCallUrls(
     formValues: ControlForm,
     nshmServices: NshmMetadata[]
   ): string[] {
@@ -175,7 +355,248 @@ export class AppFacade {
    *
    * @param location The location
    */
-  setLocation(location: Location): void {
-    this.store.dispatch(appActions.setLocation({location}));
+  public setLocation(location: Location): void {
+    this.formGroup.patchValue({
+      latitude: location.latitude,
+      longitude: location.longitude,
+    });
+  }
+
+  public updateState(state: Partial<AppState>): void {
+    this.state.set({
+      ...this.state(),
+      ...state,
+    });
+  }
+
+  private addRequiredValidator(control: AbstractControl): void {
+    control.addValidators(control => Validators.required(control));
+  }
+
+  private addValidators(): void {
+    const controls = this.formGroup.controls;
+
+    this.addRequiredValidator(controls.imt);
+    this.addRequiredValidator(controls.imt);
+    this.addRequiredValidator(controls.latitude);
+    this.addRequiredValidator(controls.longitude);
+    this.addRequiredValidator(controls.model);
+    this.addRequiredValidator(controls.modelCompare);
+    this.addRequiredValidator(controls.vs30);
+    this.addRequiredValidator(controls.returnPeriod);
+
+    controls.latitude.addValidators(validateNan());
+    controls.longitude.addValidators(validateNan());
+  }
+
+  /**
+   * Returns the combined usage from model and comparable model.
+   *
+   * @param usageResponses The usages
+   * @param model The current selected model
+   * @param comparableModel The current comparable model
+   */
+  private getCombinedUsage(
+    usageResponses: Map<string, HazardUsageResponse>,
+    model: NshmId,
+    comparableModel: string | null
+  ): HazardUsageResponse {
+    let combinedUsage = usageResponses.get(model);
+
+    if (comparableModel !== null) {
+      combinedUsage = responseHandler.combineUsages([
+        usageResponses.get(model),
+        usageResponses.get(comparableModel),
+      ]);
+    }
+
+    return combinedUsage;
+  }
+
+  /**
+   * Returns the available models that are comparable to the select model.
+   *
+   * @param state The application state
+   */
+  private getComparableModels(): Parameter[] {
+    const formValues = this.formGroup.getRawValue();
+    const state = this.state();
+
+    const currentNshmService = state.nshmServices.find(
+      nshmService => nshmService.model === formValues.model
+    );
+
+    const comparableServices = state.nshmServices.filter(
+      nshmService =>
+        nshmService.model !== currentNshmService.model &&
+        nshmService.project === currentNshmService.project
+    );
+
+    return state.availableModels.filter(model =>
+      comparableServices.find(
+        service => service.model.toString() === model.value
+      )
+    );
+  }
+
+  /**
+   * Returns the IMT to use.
+   *
+   * @param query The query value
+   * @param defaultImt The default IMT
+   */
+  private getImt(query: string, defaultImt: Imt): Imt {
+    return (
+      Object.values(Imt).find(imt => imt.toString() === query) ?? defaultImt
+    );
+  }
+
+  /**
+   * Returns the model to use.
+   *
+   * @param query The query model
+   * @param defaultModel The default model
+   */
+  private getModel(query: string, defaultModel: NshmId): NshmId {
+    return (
+      Object.values(NshmId).find(model => model.toString() === query) ??
+      defaultModel
+    );
+  }
+
+  /**
+   * Returns the site class to use.
+   *
+   * @param query The query value
+   * @param defaultSiteClass The default site class
+   */
+  private getSiteClass(
+    query: string,
+    defaultSiteClass: NehrpSiteClass
+  ): NehrpSiteClass {
+    return (
+      Object.values(NehrpSiteClass).find(
+        siteClass => siteClass.toString() === query
+      ) ?? defaultSiteClass
+    );
+  }
+
+  private initialFormSet(): void {
+    const query = this.route.snapshot.queryParams as Query;
+    const defaultValues = controlPanelFormInitialState();
+
+    console.log(query);
+
+    const formValues: ControlForm = {
+      commonReturnPeriods: defaultValues.commonReturnPeriods,
+      imt: this.getImt(query.imt, defaultValues.imt),
+      latitude: query.latitude
+        ? Number.parseFloat(query.latitude)
+        : defaultValues.latitude,
+      longitude: query.longitude
+        ? Number.parseFloat(query.longitude)
+        : defaultValues.longitude,
+      maxDirection:
+        query.maxDirection !== undefined
+          ? (JSON.parse(query.maxDirection) as boolean)
+          : defaultValues.maxDirection,
+      model: this.getModel(query.model, defaultValues.model),
+      modelCompare: this.getModel(
+        query.modelCompare,
+        defaultValues.modelCompare
+      ),
+      returnPeriod: query.returnPeriod
+        ? Number.parseInt(query.returnPeriod, 10)
+        : defaultValues.returnPeriod,
+      siteClass: this.getSiteClass(
+        query.siteClass,
+        defaultValues.siteClass as NehrpSiteClass
+      ),
+      sourceType: null,
+      truncate:
+        query.truncate !== undefined
+          ? (JSON.parse(query.truncate) as boolean)
+          : defaultValues.truncate,
+      vs30: query.vs30 ? Number.parseFloat(query.vs30) : defaultValues.vs30,
+    };
+
+    this.formGroup.patchValue(formValues);
+
+    if (this.formGroup.valid) {
+      this.nshmpService.selectPlotControl();
+      this.callServices();
+    } else if (this.formGroup.getRawValue() !== defaultValues) {
+      this.formGroup.markAsDirty();
+    }
+
+    this.formGroup.valueChanges.subscribe(() => this.updateUrl());
+  }
+
+  /**
+   * Create the response spectra for a specific model and for each source type available.
+   *
+   * @param hazardResponses The hazard responses
+   * @param form The control form values
+   */
+  private responseSpectra(
+    hazardResponses: HazardResponse[],
+    form: ControlForm
+  ): Spectra[] {
+    const spectra: Spectra[] = [];
+    const sourceTypes = [...hazardResponses]
+      .shift()
+      .data.map(data => data.component);
+
+    sourceTypes.forEach(sourceType => {
+      form.sourceType = sourceType;
+      spectra.push({
+        responseSpectra: hazardUtils.responseSpectra(hazardResponses, form),
+        sourceType: sourceTypeFromPascalCase(sourceType),
+      });
+    });
+
+    return spectra;
+  }
+
+  /**
+   * Returns the URL to call for a specific model.
+   *
+   * @param model The NSHM to call
+   * @param formValues The control panel form values
+   * @param nshmServices The NSHM service metadata
+   */
+  private serviceCallUrl(
+    model: NshmId,
+    formValues: ControlForm,
+    nshmServices: NshmMetadata[]
+  ): string {
+    const {latitude, longitude, vs30} = formValues;
+    const service = nshmServices.find(service => service.model === model);
+    return `${service.url}${this.serviceEndpoint}/${longitude}/${latitude}/${vs30}`;
+  }
+
+  private updateUrl(): void {
+    const value = this.formGroup.getRawValue();
+
+    const query: Query = {
+      imt: value.imt,
+      latitude: value.latitude?.toString(),
+      longitude: value.longitude?.toString(),
+      maxDirection: String(value.maxDirection),
+      model: value.model,
+      modelCompare: value.modelCompare,
+      returnPeriod: value.returnPeriod?.toString(),
+      siteClass: value.siteClass,
+      truncate: String(value.truncate),
+      vs30: value.vs30?.toString(),
+    };
+
+    this.router
+      .navigate([devApps().hazard.dynamicCompare.routerLink], {
+        queryParams: {
+          ...query,
+        },
+      })
+      .catch((error: Error) => this.nshmpService.throwError$(error));
   }
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.reducer.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.reducer.ts
deleted file mode 100644
index 407ad601086e6ed8fafa776c67b5687363ce8ce9..0000000000000000000000000000000000000000
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.reducer.ts
+++ /dev/null
@@ -1,352 +0,0 @@
-import {HazardFormControlIds, hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {nshmpUtils} from '@ghsc/nshmp-lib-ng/nshmp';
-import {HazardUsageResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
-import {NehrpSiteClass} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
-import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
-import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {createFeature, createReducer, createSelector, on} from '@ngrx/store';
-import {
-  onNgrxForms,
-  onNgrxFormsAction,
-  setUserDefinedProperty,
-  setValue,
-  SetValueAction,
-  updateGroup,
-  validate,
-  wrapReducerWithFormStateUpdate,
-} from 'ngrx-forms';
-import {required} from 'ngrx-forms/validation';
-import {
-  onPlotRedraw,
-  onPlots,
-  onPlotSettingsForm,
-} from 'projects/nshmp-apps/src/shared/state/shared';
-
-import {PlotSettingsId} from '../utils/app.utils';
-import * as responseHandler from '../utils/response-handler.utils';
-import {appActions} from './app.actions';
-import {AppState, ControlForm, FORM_ID, initialState} from './app.state';
-
-/**
- * NGRX form ids
- */
-const formKeys = {
-  controlPanel: {
-    commonReturnPeriods: `${FORM_ID}.${HazardFormControlIds.COMMON_RETURN_PERIODS}`,
-    imt: `${FORM_ID}.${HazardFormControlIds.IMT}`,
-    maxDirection: `${FORM_ID}.${HazardFormControlIds.MAX_DIRECTION}`,
-    model: `${FORM_ID}.${HazardFormControlIds.MODEL}`,
-    modelCompare: `${FORM_ID}.modelCompare`,
-    returnPeriod: `${FORM_ID}.${HazardFormControlIds.RETURN_PERIOD}`,
-    siteClass: `${FORM_ID}.${HazardFormControlIds.SITE_CLASS}`,
-    truncate: `${FORM_ID}.${HazardFormControlIds.TRUNCATE}`,
-    vs30: `${FORM_ID}.${HazardFormControlIds.VS30}`,
-  },
-};
-
-/**
- * NGRX state feature with reducer and selectors.
- */
-export const dynamicCompareAppFeature = createFeature({
-  // Add additional selectors
-  extraSelectors: ({
-    selectControlPanelForm,
-    selectNshmServices,
-    selectUsageResponses,
-  }) => ({
-    /** Select `NshmService` of current model */
-    selectNshmService: createSelector(
-      selectNshmServices,
-      selectControlPanelForm,
-      (nshmServices, controlPanelForm) =>
-        nshmServices?.find(
-          nshmService => nshmService?.model === controlPanelForm.value.model
-        )
-    ),
-    /** Select model A usage */
-    selectUsageModelA: createSelector(
-      selectControlPanelForm,
-      selectUsageResponses,
-      (controlPanel, usages) => usages?.get(controlPanel.value.model)
-    ),
-    /** Select model B usage */
-    selectUsageModelB: createSelector(
-      selectControlPanelForm,
-      selectUsageResponses,
-      (controlPanel, usages) => usages?.get(controlPanel.value.modelCompare)
-    ),
-  }),
-  // Applicaiton name
-  name: 'dynamicCompareApp',
-  // NGRX reducer
-  reducer: createReducer(
-    // Initial state
-    initialState(),
-    // On NGRX forms
-    onNgrxForms(),
-    // Handle NGRX form changes
-    onNgrxFormsAction(SetValueAction, (state, action) =>
-      onFormAction(state, action)
-    ),
-    // On available models action
-    on(appActions.availableModels, (state, {models}) => ({
-      ...state,
-      availableModels: [...models],
-    })),
-    // Handle initialization
-    on(appActions.init, state => ({
-      ...state,
-    })),
-    // Handle NSHM services action
-    on(appActions.nshmServices, (state, {nshmServices}) => ({
-      ...state,
-      nshmServices,
-    })),
-    // On plots action
-    on(appActions.plots, (state, {plots}) => onPlots(state, plots)),
-    // On plot redraw action
-    on(appActions.plotRedraw, state => onPlotRedraw(state)),
-    // Reset the control panel
-    on(appActions.resetControlPanel, state => {
-      state = {
-        ...state,
-        controlPanelForm: initialState().controlPanelForm,
-      };
-
-      return {
-        ...state,
-        ...onModelChange(state),
-        plots: initialState().plots,
-        serviceCallInfo: initialState().serviceCallInfo,
-        serviceResponses: initialState().serviceResponses,
-      };
-    }),
-    // On service call info action
-    on(appActions.serviceCallInfo, (state, {serviceCallInfo}) => ({
-      ...state,
-      serviceCallInfo: {
-        ...state.serviceCallInfo,
-        ...serviceCallInfo,
-      },
-    })),
-    // Set the service responses
-    on(appActions.serviceResponses, (state, {serviceResponses}) => ({
-      ...state,
-      serviceResponses,
-    })),
-    // On usage response action
-    on(appActions.usageResponses, (state, {usageResponses}) => ({
-      ...state,
-      usageResponses,
-    }))
-  ),
-});
-
-// Add validators
-dynamicCompareAppFeature.reducer = wrapReducerWithFormStateUpdate(
-  dynamicCompareAppFeature.reducer,
-  state => state.controlPanelForm,
-  updateGroup<ControlForm>({
-    ...hazardUtils.hazardValidateControlPanelForm(),
-    sourceType: validate([required]),
-    vs30: nshmpUtils.validateBounds,
-  })
-);
-
-/**
- * Returns the available models that are comparable to the select model.
- *
- * @param state The application state
- */
-function getComparableModels(state: AppState): Parameter[] {
-  const formValues = state.controlPanelForm.value;
-
-  const currentNshmService = state.nshmServices.find(
-    nshmService => nshmService.model === formValues.model
-  );
-
-  const comparableServices = state.nshmServices.filter(
-    nshmService =>
-      nshmService.model !== currentNshmService.model &&
-      nshmService.project === currentNshmService.project
-  );
-
-  return state.availableModels.filter(model =>
-    comparableServices.find(service => service.model.toString() === model.value)
-  );
-}
-
-/**
- * Returns the combined usage from model and comparable model.
- *
- * @param usageResponses The usages
- * @param model The current selected model
- * @param comparableModel The current comparable model
- */
-function getCombinedUsage(
-  usageResponses: Map<string, HazardUsageResponse>,
-  model: NshmId,
-  comparableModel: string | null
-): HazardUsageResponse {
-  let combinedUsage = usageResponses.get(model);
-
-  if (comparableModel !== null) {
-    combinedUsage = responseHandler.combineUsages([
-      usageResponses.get(model),
-      usageResponses.get(comparableModel),
-    ]);
-  }
-
-  return combinedUsage;
-}
-
-/**
- * Handle form actions.
- *
- * @param state The current state
- * @param action The action
- */
-function onFormAction(
-  state: AppState,
-  action: SetValueAction<unknown>
-): AppState {
-  const formValues = state.controlPanelForm.value;
-
-  switch (action.controlId) {
-    case formKeys.controlPanel.model: {
-      return onModelChange(state);
-    }
-    case formKeys.controlPanel.modelCompare: {
-      if (action.value === null) {
-        return onModelChange(state);
-      }
-
-      const usages = [
-        state.usageResponses.get(formValues.model),
-        state.usageResponses.get(formValues.modelCompare),
-      ];
-
-      return {
-        ...state,
-        combinedUsage: responseHandler.combineUsages(usages),
-      };
-    }
-    case formKeys.controlPanel.commonReturnPeriods: {
-      const updatedState: AppState = {
-        ...state,
-        controlPanelForm: hazardUtils.onCommonReturnPeriodChange(
-          state.controlPanelForm
-        ),
-      };
-
-      return {
-        ...updatedState,
-        plots: responseHandler.createPlots(updatedState),
-      };
-    }
-    case formKeys.controlPanel.returnPeriod: {
-      const updatedState: AppState = {
-        ...state,
-        controlPanelForm: hazardUtils.onReturnPeriodChange(
-          state.controlPanelForm
-        ),
-      };
-
-      return {
-        ...updatedState,
-        plots: responseHandler.createPlots(updatedState),
-      };
-    }
-    case formKeys.controlPanel.imt:
-    case formKeys.controlPanel.maxDirection:
-    case formKeys.controlPanel.truncate: {
-      return {
-        ...state,
-        plots: responseHandler.createPlots(state),
-      };
-    }
-    case formKeys.controlPanel.siteClass: {
-      return {
-        ...state,
-        controlPanelForm: hazardUtils.onSiteClassChange(
-          state.controlPanelForm,
-          state.combinedUsage.response.model.siteClasses
-        ),
-        plots: initialState().plots,
-        serviceResponses: initialState().serviceResponses,
-      };
-    }
-    case formKeys.controlPanel.vs30: {
-      return {
-        ...state,
-        controlPanelForm: hazardUtils.onVs30Change(
-          state.controlPanelForm,
-          state.combinedUsage.response.model.siteClasses
-        ),
-        plots: initialState().plots,
-      };
-    }
-    default: {
-      if (
-        Object.values(PlotSettingsId).some(id => action.controlId.includes(id))
-      ) {
-        return onPlotSettingsForm(state, action);
-      } else {
-        return {
-          ...state,
-        };
-      }
-    }
-  }
-}
-
-/**
- * Returns the updated state for a model change.
- *
- * @param state The current application state
- */
-function onModelChange(state: AppState): AppState {
-  const formValues = state.controlPanelForm.value;
-
-  const comparableModels = getComparableModels(state);
-  const defaultComparableModel = [...comparableModels]?.pop()?.value ?? null;
-  const combinedUsage = getCombinedUsage(
-    state.usageResponses,
-    formValues.model,
-    defaultComparableModel
-  );
-
-  const siteClasses = combinedUsage.response.model.siteClasses;
-
-  let controlPanelForm = updateGroup<ControlForm>({
-    modelCompare: modelCompare =>
-      setValue(modelCompare, defaultComparableModel),
-    vs30: vs30 =>
-      setUserDefinedProperty(
-        vs30,
-        'bounds',
-        hazardUtils.vs30Bounds(combinedUsage)
-      ),
-  })(state.controlPanelForm);
-
-  controlPanelForm = hazardUtils.onModelChange(
-    controlPanelForm,
-    Object.keys(siteClasses) as NehrpSiteClass[],
-    hazardUtils.locationBounds(combinedUsage)
-  );
-
-  controlPanelForm = hazardUtils.onVs30Change(controlPanelForm, siteClasses);
-
-  return {
-    ...state,
-    combinedUsage,
-    comparableModels,
-    controlPanelForm,
-    plots: initialState().plots,
-    serviceCallInfo: {
-      ...state.serviceCallInfo,
-      serviceCalls: [],
-    },
-    serviceResponses: initialState().serviceResponses,
-  };
-}
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts
index e493ca762039d97dc9ff076bb1d88da07bb94734..f96ab64ad0aa26c97fd95b3675579fc4670128ce 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/state/app.state.ts
@@ -1,9 +1,10 @@
 import {
   DynamicHazardControlForm,
-  hazardUtils,
   ResponseSpectra,
 } from '@ghsc/nshmp-lib-ng/hazard';
-import {ServiceCallInfo} from '@ghsc/nshmp-lib-ng/nshmp';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {ServiceCallInfo} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {
   HazardCalcResponse,
   HazardUsageResponse,
@@ -16,8 +17,6 @@ import {
 } from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/model';
 import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {createFormGroupState, FormGroupState, FormState} from 'ngrx-forms';
-import {SharedAppState} from 'projects/nshmp-apps/src/shared/state/shared';
 
 import {defaultPlots} from '../utils/app.utils';
 
@@ -39,17 +38,17 @@ export enum Plots {
 /**
  * Development dynamic hazard compare NGRX state.
  */
-export interface AppState extends SharedAppState {
+export interface AppState {
   /** Available NSHMs */
   availableModels: Parameter[];
   /** Common usage for selected models */
   combinedUsage: HazardUsageResponse;
   /** Models that can compare with a selected model */
   comparableModels: Parameter[];
-  /** Control panel form field state */
-  controlPanelForm: FormState<ControlForm>;
   /** NSHM service metadata */
   nshmServices: NshmMetadata[];
+  /** The application's plots */
+  plots: Map<string, NshmpPlot>;
   /** Service call info */
   serviceCallInfo: ServiceCallInfo;
   /** Hazard service responses */
@@ -108,13 +107,12 @@ export function initialState(): AppState {
     availableModels: [],
     combinedUsage: null,
     comparableModels: [],
-    controlPanelForm: controlPanelFormInitialState(),
     nshmServices: [],
     plots: defaultPlots(),
     serviceCallInfo: {
       serviceCalls: [],
       serviceName: 'Dynamic Hazard Curves',
-      usage: null,
+      usage: [],
     },
     serviceResponses: {
       modelA: null,
@@ -127,12 +125,12 @@ export function initialState(): AppState {
 /**
  * Initial state for the control panel,
  */
-export function controlPanelFormInitialState(): FormGroupState<ControlForm> {
-  return createFormGroupState<ControlForm>(FORM_ID, {
+export function controlPanelFormInitialState(): ControlForm {
+  return {
     ...hazardUtils.hazardDefaultFormValues(),
     imt: Imt.PGA,
     modelCompare: null,
     sourceType: sourceTypeToCapitalCase(SourceType.TOTAL),
     vs30: 760,
-  });
+  };
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts
index e7f6b20a24512f9b0c6ddeeb07d30ed975d5b78b..fe276caedd5f5dd0c1de77f547c808d4c85e290f 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/app.utils.ts
@@ -1,38 +1,22 @@
-import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {
-  NshmpPlot,
-  NshmpPlotSettings,
-  PlotOptions,
-  plotUtils,
-} from '@ghsc/nshmp-lib-ng/plot';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {NshmpPlot, PlotOptions, plotUtils} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {NshmId} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/nshm';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
-import {createFormGroupState} from 'ngrx-forms';
 
 import {Plots} from '../state/app.state';
 
-/**
- * Plot ngrx-form ids.
- */
-export enum PlotSettingsId {
-  HAZARD = 'ngrx-forms [Hazard Curves Settings]',
-  HAZARD_COMPONENTS = 'ngrx-forms [Hazard Component Curves Settings]',
-  HAZARD_DIFFERENCES = 'ngrx-forms [Hazard Difference Settings]',
-  SPECTRA = 'ngrx-forms [Spectra Settings]',
-  SPECTRA_COMPONENTS = 'ngrx-forms [Spectra Components Settings]',
-  SPECTRA_DIFFERENCES = 'ngrx-forms [Spectra Difference Settings]',
-}
-
 const differenceAspectRatio = '3:1';
 const hazardXLabel = 'Ground Motion (g)';
 const hazardYLabel = 'Annual Frequency of Exceedence';
 const spectraXLabel = 'Spectral Period (s)';
 const spectraYLabel = hazardXLabel;
+
 const plotOptions: PlotOptions = {
   layout: {
     aspectRatio: hazardUtils.HAZARD_ASPECT_RATIO,
   },
 };
+
 const diffPlotOptions: PlotOptions = {
   layout: {
     aspectRatio: differenceAspectRatio,
@@ -113,13 +97,10 @@ function setHazardPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.HAZARD, {
     label: 'Hazard Curves',
     plotData: hazardPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.HAZARD,
-      {
-        config: hazardPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(hazardPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: hazardPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(hazardPlotData.layout),
+    }),
   });
 
   const hazardDiffPlotData = plotUtils.defaultPlot({
@@ -134,13 +115,10 @@ function setHazardPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.HAZARD_DIFFERENCES, {
     label: 'Hazard Curve Differences',
     plotData: hazardDiffPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.HAZARD_DIFFERENCES,
-      {
-        config: hazardDiffPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(hazardDiffPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: hazardDiffPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(hazardDiffPlotData.layout),
+    }),
   });
 
   const hazardComponentsPlotData = plotUtils.defaultPlot({
@@ -155,15 +133,10 @@ function setHazardPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.HAZARD_COMPONENTS, {
     label: 'Hazard Component Curves',
     plotData: hazardComponentsPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.HAZARD_COMPONENTS,
-      {
-        config: hazardComponentsPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(
-          hazardComponentsPlotData.layout
-        ),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: hazardComponentsPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(hazardComponentsPlotData.layout),
+    }),
   });
 }
 
@@ -185,13 +158,10 @@ function setSpectraPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.SPECTRUM, {
     label: 'Spectrum',
     plotData: spectrumPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.SPECTRA,
-      {
-        config: spectrumPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(spectrumPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: spectrumPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(spectrumPlotData.layout),
+    }),
   });
 
   const spectrumDiffPlotData = plotUtils.defaultPlot({
@@ -206,13 +176,10 @@ function setSpectraPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.SPECTRUM_DIFFERENCES, {
     label: 'Spectrum Differences',
     plotData: spectrumDiffPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.SPECTRA_DIFFERENCES,
-      {
-        config: spectrumDiffPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(spectrumDiffPlotData.layout),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: spectrumDiffPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(spectrumDiffPlotData.layout),
+    }),
   });
 
   const spectraComponentsPlotData = plotUtils.defaultPlot({
@@ -227,14 +194,11 @@ function setSpectraPlots(plots: Map<string, NshmpPlot>): void {
   plots.set(Plots.SPECTRUM_COMPONENTS, {
     label: 'Spectra Component Curves',
     plotData: spectraComponentsPlotData,
-    settingsForm: createFormGroupState<NshmpPlotSettings>(
-      PlotSettingsId.SPECTRA_COMPONENTS,
-      {
-        config: spectraComponentsPlotData.config,
-        layout: plotUtils.plotlyLayoutToSettings(
-          spectraComponentsPlotData.layout
-        ),
-      }
-    ),
+    settingsForm: plotUtils.plotSettingsToFormGroup({
+      config: spectraComponentsPlotData.config,
+      layout: plotUtils.plotlyLayoutToSettings(
+        spectraComponentsPlotData.layout
+      ),
+    }),
   });
 }
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts
index 1182fbcecb119d2ffc3eeffcfb52f49c72bdb2ab..e480a7c02610a797c626ae30918a27d71c49a522 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/hazard-plots.utils.ts
@@ -1,6 +1,6 @@
-import {hazardUtils} from '@ghsc/nshmp-lib-ng/hazard';
-import {NumberBounds} from '@ghsc/nshmp-lib-ng/nshmp';
-import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-ng/plot';
+import {hazardUtils} from '@ghsc/nshmp-lib-no-ngrx/hazard';
+import {FormGroupControls, NumberBounds} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot, plotUtils} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {
   HazardCurve,
   HazardResponse,
@@ -82,7 +82,10 @@ interface PlotDash {
  *
  * @param state The app state
  */
-export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
+export function createHazardPlots(
+  state: AppState,
+  formGroup: FormGroupControls<ControlForm>
+): Map<string, NshmpPlot> {
   const plots = new Map<string, NshmpPlot>();
 
   const hazardPlot = state.plots.get(Plots.HAZARD);
@@ -93,7 +96,7 @@ export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
     state.serviceResponses,
     state.availableModels,
     hazardPlot,
-    state.controlPanelForm.value
+    formGroup.getRawValue()
   );
 
   plots.set(Plots.HAZARD, {
@@ -103,7 +106,7 @@ export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
 
   const hazardCurveDifference = createHazardDiffPlot(
     state.serviceResponses,
-    state.controlPanelForm.value.imt,
+    formGroup.getRawValue().imt,
     hazardDiffPlot
   );
 
@@ -116,7 +119,7 @@ export function createHazardPlots(state: AppState): Map<string, NshmpPlot> {
     state.serviceResponses,
     state.availableModels,
     hazardComponentsPlot,
-    state.controlPanelForm.value
+    formGroup.getRawValue()
   );
 
   plots.set(Plots.HAZARD_COMPONENTS, {
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts
index 5de1076c701ac81a87bf7eee32de7dd0c3f5f92d..c393e7ad8cf154efc1593a5416255b1bb6253cb1 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/response-handler.utils.ts
@@ -1,8 +1,9 @@
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
+import {FormGroupControls} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {HazardUsageResponse} from '@ghsc/nshmp-utils-ts/libs/nshmp-haz/www/hazard-service';
 import {Parameter} from '@ghsc/nshmp-utils-ts/libs/nshmp-ws-utils/metadata';
 
-import {AppState} from '../state/app.state';
+import {AppState, ControlForm} from '../state/app.state';
 import {createHazardPlots} from './hazard-plots.utils';
 import {createSpectraPlots} from './spectra-plots.utils';
 
@@ -59,7 +60,10 @@ export function combineUsages(
  *
  * @param state The app state
  */
-export function createPlots(state: AppState): Map<string, NshmpPlot> {
+export function createPlots(
+  state: AppState,
+  formGroup: FormGroupControls<ControlForm>
+): Map<string, NshmpPlot> {
   if (
     state.serviceResponses.modelA === null ||
     state.serviceResponses.modelB === null
@@ -67,8 +71,8 @@ export function createPlots(state: AppState): Map<string, NshmpPlot> {
     return state.plots;
   }
 
-  const hazardPlots = createHazardPlots(state);
-  const spectraPlots = createSpectraPlots(state);
+  const hazardPlots = createHazardPlots(state, formGroup);
+  const spectraPlots = createSpectraPlots(state, formGroup);
   return new Map([...hazardPlots.entries(), ...spectraPlots.entries()]);
 }
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts
index 7f4d63fbc8de9544240f969a3f742a76a4f821e8..39cdd9962f0bb0cc6622d4beb0a5c141c6e1c5f2 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/dynamic-compare/utils/spectra-plots.utils.ts
@@ -1,4 +1,5 @@
-import {NshmpPlot} from '@ghsc/nshmp-lib-ng/plot';
+import {FormGroupControls} from '@ghsc/nshmp-lib-no-ngrx/nshmp';
+import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 import {Imt, imtToPeriod} from '@ghsc/nshmp-utils-ts/libs/nshmp-lib/gmm';
 import {
   SourceType,
@@ -110,7 +111,10 @@ interface PlotColors {
  *
  * @param state The app state
  */
-export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
+export function createSpectraPlots(
+  state: AppState,
+  formGroup: FormGroupControls<ControlForm>
+): Map<string, NshmpPlot> {
   const plots = new Map<string, NshmpPlot>();
 
   const spectraPlot = state.plots.get(Plots.SPECTRUM);
@@ -120,7 +124,7 @@ export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
   const spectraCurves = createSpectraCurvesPlot(
     state.serviceResponses,
     state.availableModels,
-    state.controlPanelForm.value,
+    formGroup.getRawValue(),
     spectraPlot
   );
 
@@ -132,7 +136,7 @@ export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
   const spectraComponents = createSpectraComponentsPlot(
     state.serviceResponses,
     state.availableModels,
-    state.controlPanelForm.value,
+    formGroup.getRawValue(),
     spectraPlot
   );
 
@@ -144,7 +148,7 @@ export function createSpectraPlots(state: AppState): Map<string, NshmpPlot> {
   const spectraDiff = createSpectraDiffPlot(
     state.serviceResponses,
     state.availableModels,
-    state.controlPanelForm.value,
+    formGroup.getRawValue(),
     spectraDiffPlot
   );
 
diff --git a/projects/nshmp-apps/src/app/dev/hazard/hazard.routes.ts b/projects/nshmp-apps/src/app/dev/hazard/hazard.routes.ts
index 37259a446d29f50c4ffa28b662455f70ec89ec6c..5e7bea6247cfd40f5d9165367d3cae50b7e445bf 100644
--- a/projects/nshmp-apps/src/app/dev/hazard/hazard.routes.ts
+++ b/projects/nshmp-apps/src/app/dev/hazard/hazard.routes.ts
@@ -1,11 +1,7 @@
 import {inject} from '@angular/core';
 import {Routes} from '@angular/router';
-import {provideEffects} from '@ngrx/effects';
-import {provideState} from '@ngrx/store';
 
 import {DynamicHazardCompareGuard} from './dynamic-compare/guards/dynamic-hazard-compare.guard';
-import {DynamicCompareAppEffects} from './dynamic-compare/state/app.effects';
-import {dynamicCompareAppFeature} from './dynamic-compare/state/app.reducer';
 
 /** Routes for dev hazard applications */
 const routes: Routes = [
@@ -14,10 +10,6 @@ const routes: Routes = [
     loadComponent: () =>
       import('./dynamic-compare/app.component').then(com => com.AppComponent),
     path: 'dynamic-compare',
-    providers: [
-      provideState(dynamicCompareAppFeature),
-      provideEffects(DynamicCompareAppEffects),
-    ],
   },
 ];
 
diff --git a/projects/nshmp-apps/src/shared/models/plot-settings-panel.model.ts b/projects/nshmp-apps/src/shared/models/plot-settings-panel.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..641b80c49384b755340ea4f69414ebc8e3887c23
--- /dev/null
+++ b/projects/nshmp-apps/src/shared/models/plot-settings-panel.model.ts
@@ -0,0 +1,5 @@
+import {PlotSettingsPanel} from '@ghsc/nshmp-lib-no-ngrx/plot';
+
+export interface PlotSettingsPanelId extends PlotSettingsPanel {
+  id: string;
+}
diff --git a/projects/nshmp-apps/src/shared/utils/facade.utils.ts b/projects/nshmp-apps/src/shared/utils/facade.utils.ts
index a1ff3e61b0d01d38538a06ede5c14a3111661626..bb3ba5d49fa434844b96c449aef62e3c39ae0809 100644
--- a/projects/nshmp-apps/src/shared/utils/facade.utils.ts
+++ b/projects/nshmp-apps/src/shared/utils/facade.utils.ts
@@ -1,3 +1,4 @@
+import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';
 import {NshmpPlot} from '@ghsc/nshmp-lib-no-ngrx/plot';
 
 export interface ResetPlotSettingsProps {
@@ -34,3 +35,11 @@ export function redrawPlots(
 
   return updatedPlots;
 }
+
+export function validateNan(): ValidatorFn {
+  return (
+    control: AbstractControl<number, number>
+  ): ValidationErrors | null => {
+    return isNaN(control.value) ? {nan: {value: control.value}} : null;
+  };
+}