diff --git a/package.json b/package.json
index 8c2546d26cc114a0336d4ac28d8ec2a7b7396c79..5ecc49077569747c214bf18d3e29aa01f07a5b98 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
     "@ngx-translate/core": "^14.0.0",
     "@ngx-translate/http-loader": "^7.0.0",
     "@ionic/pwa-elements": "~3.1.1",
+    "@rx-angular/state": "1.7.0",
     "@polkadot/api": "^9.2.4",
     "@polkadot/keyring": "^10.1.6",
     "@polkadot/networks": "^10.1.6",
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 88f43782b5cd76e42013bd71c59a3a55b5685890..239a2167d97ec16fcc5f647a2d33bf90b6f61bc4 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -26,7 +26,12 @@ const routes: Routes = [
   {
     path: 'settings',
     loadChildren: () => import('./settings/settings.module').then( m => m.SettingsPageModule)
-  }
+  },
+  // DEV only
+  {
+    path: 'playground',
+    loadChildren: () => import('./playground/playground.module').then( m => m.PlaygroundModule)
+  },
 ];
 
 @NgModule({
diff --git a/src/app/playground/playground-routing.module.ts b/src/app/playground/playground-routing.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66e5d776c2c22e0e8a5068bc698ea1894722b5ab
--- /dev/null
+++ b/src/app/playground/playground-routing.module.ts
@@ -0,0 +1,18 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {HomePage} from "@app/home/home.page";
+import {PlaygroundPage} from "@app/playground/playground.page";
+
+const routes: Routes = [
+  {
+    path: '',
+    pathMatch: 'full',
+    component: PlaygroundPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class PlaygroundPageRoutingModule {}
diff --git a/src/app/playground/playground.module.ts b/src/app/playground/playground.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..99ffa8078d170aa20fda940343c7082d675014f5
--- /dev/null
+++ b/src/app/playground/playground.module.ts
@@ -0,0 +1,19 @@
+import {NgModule} from "@angular/core";
+import {AppSharedModule} from "@app/shared/shared.module";
+import {TranslateModule} from "@ngx-translate/core";
+import {AppAuthModule} from "@app/auth/auth.module";
+import {AppRegisterModule} from "@app/register/register.module";
+import {PlaygroundPage} from "@app/playground/playground.page";
+import {PlaygroundPageRoutingModule} from "@app/playground/playground-routing.module";
+
+@NgModule({
+  imports: [
+    AppSharedModule,
+    TranslateModule.forChild(),
+    PlaygroundPageRoutingModule,
+    AppAuthModule,
+    AppRegisterModule
+  ],
+  declarations: [PlaygroundPage]
+})
+export class PlaygroundModule {}
diff --git a/src/app/playground/playground.page.html b/src/app/playground/playground.page.html
new file mode 100644
index 0000000000000000000000000000000000000000..d41d3da40e0bacaf3075ca7bfe9a36053c4d31cd
--- /dev/null
+++ b/src/app/playground/playground.page.html
@@ -0,0 +1,8 @@
+
+<ion-content>
+
+  <h1>Playground</h1>
+
+  <div>{{ state$ | async | json }}</div>
+
+</ion-content>
diff --git a/src/app/playground/playground.page.ts b/src/app/playground/playground.page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f0edd3e8a93234fae142705c4480b29e52e6c1c6
--- /dev/null
+++ b/src/app/playground/playground.page.ts
@@ -0,0 +1,35 @@
+import {Component, Injector} from "@angular/core";
+import {RxBasePage} from "@app/playground/rx-base.page";
+import {RxState} from "@rx-angular/state";
+import {interval, map, mapTo} from "rxjs";
+
+
+export declare interface PlaygroundState {
+  bar: number;
+  foo: string;
+}
+
+@Component({
+  selector: 'app-playground',
+  templateUrl: './playground.page.html',
+  //styleUrls: ['./playground.page.scss']
+  providers: [RxState]
+})
+export class PlaygroundPage extends RxBasePage<PlaygroundState> {
+  readonly state$ = this.state.select();
+
+  constructor(injector: Injector,
+              private state: RxState<PlaygroundState>) {
+    super(injector);
+    state.set({bar: 0, foo: 'foo'})
+    const sliceToAdd$ = interval(250)
+      .pipe(map((i) => {
+        return { bar: 5 * i, foo: 'foo'};
+      }));
+    state.connect(sliceToAdd$);
+  }
+
+  protected ngOnLoad(): Promise<PlaygroundState> {
+    return Promise.resolve(undefined);
+  }
+}
diff --git a/src/app/playground/rx-base.page.ts b/src/app/playground/rx-base.page.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f980796cc6c40bd657621e7ccf19db6cec5695c
--- /dev/null
+++ b/src/app/playground/rx-base.page.ts
@@ -0,0 +1,170 @@
+import {ChangeDetectorRef, Directive, inject, Injector, OnDestroy, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {SettingsService} from "@app/settings/settings.service";
+import {changeCaseToUnderscore, isNotNilOrBlank} from "@app/shared/functions";
+import {environment} from "@environments/environment";
+import {waitIdle} from "@app/shared/forms";
+import {WaitForOptions} from "@app/shared/observables";
+import {ToastController, ToastOptions} from "@ionic/angular";
+import {TranslateService} from "@ngx-translate/core";
+import {Subscription} from "rxjs";
+
+export interface BasePageOptions {
+  name: string;
+  loadDueTime: number;
+}
+
+@Directive()
+export abstract class RxBasePage<
+  S,
+  O extends BasePageOptions = BasePageOptions
+  >
+  implements OnInit, OnDestroy {
+
+  private _cd: ChangeDetectorRef;
+  private _subscription: Subscription;
+
+  protected translate: TranslateService;
+  protected settings: SettingsService;
+  protected readonly activatedRoute: ActivatedRoute;
+  protected toastController: ToastController;
+  protected readonly _debug = !environment.production;
+  protected readonly _logPrefix: string;
+  protected readonly _options: O;
+  protected _presentingElement: Element = null;
+
+  mobile: boolean = null;
+  error: string = null;
+  loading = true;
+  data: S = null;
+
+  get loaded(): boolean {
+    return !this.loading;
+  }
+
+  protected constructor(
+    injector: Injector,
+    options?: Partial<O>
+  ) {
+    this._cd = injector.get(ChangeDetectorRef);
+    this.settings = injector.get(SettingsService);
+    this.translate = injector.get(TranslateService);
+    this.activatedRoute = injector.get(ActivatedRoute);
+    this.toastController = injector.get(ToastController);
+    this.mobile = this.settings.mobile;
+    this._options = <O>{
+      name: options?.name || changeCaseToUnderscore(this.constructor.name).replace(/_/g, '-'),
+      loadDueTime: 0,
+      ...options
+    };
+    this._logPrefix = `[${this._options.name}] `;
+  }
+
+  ngOnInit() {
+
+    // Get modal presenting element (iOS only)
+    this._presentingElement = this.mobile ? document.querySelector('.ion-page') : null;
+
+    // Load data
+    setTimeout(() => this.load(), this._options.loadDueTime || 0);
+  }
+
+  ngOnDestroy() {
+    console.debug(`${this._logPrefix}Destroy`);
+    this._subscription?.unsubscribe();
+  }
+
+  protected async load() {
+    this.resetError();
+    this.markAsLoading();
+
+    try {
+
+      this.data = await this.ngOnLoad();
+
+      this.markForCheck();
+      this.markAsLoaded();
+    }
+    catch (err) {
+      this.setError(err);
+      this.markAsLoaded();
+    }
+  }
+
+  protected abstract ngOnLoad(): Promise<S>;
+
+  protected setError(err, opts =  {emitEvent: true}) {
+    let message = err?.message || err || 'ERROR.UNKNOWN_ERROR';
+    if (!message) {
+      console.error(err);
+      message = 'ERROR.UNKNOWN_ERROR';
+    }
+    this.error = message;
+    if (opts.emitEvent !== false) this.markForCheck();
+  }
+
+  protected resetError(opts ={emitEvent: true}) {
+    if (this.error) {
+      this.error = null;
+      if (opts.emitEvent !== false) this.markForCheck();
+    }
+  }
+
+  protected markAsLoading(opts =  {emitEvent: true}) {
+    if (!this.loading) {
+      this.loading = true;
+      if (opts.emitEvent !== false) this.markForCheck();
+    }
+  }
+
+  protected markAsLoaded(opts =  {emitEvent: true}) {
+    if (this.loading) {
+      this.loading = false;
+      if (opts.emitEvent !== false) this.markForCheck();
+    }
+  }
+
+  protected async waitIdle(opts?: WaitForOptions) {
+    return waitIdle(this, opts);
+  }
+
+  protected markForCheck() {
+    this._cd?.markForCheck();
+  }
+
+  protected debug(msg, ...params: any[]) {
+    if (!this._debug) return;
+    if (params) console.debug(this._logPrefix + msg, params);
+    else console.debug(this._logPrefix + msg)
+  }
+
+  protected info(msg, ...params: any[]) {
+    if (params) console.info(this._logPrefix + msg, params);
+    else console.info(this._logPrefix + msg)
+  }
+
+  protected log(msg, ...params: any[]) {
+    if (!this._debug) return;
+    if (params) console.log(this._logPrefix + msg, params);
+    else console.log(this._logPrefix + msg)
+  }
+
+  protected async showToast(opts: ToastOptions & {messageParams?: any[] }) {
+    const message = isNotNilOrBlank(opts?.message) ? this.translate.instant(opts.message as string, opts.messageParams) : undefined;
+    const toast = await this.toastController.create({
+      duration: 2000,
+      ...opts,
+      message
+    });
+    return toast.present();
+  }
+
+  protected registerSubscription(sub: Subscription) {
+    if (!this._subscription) this._subscription = new Subscription();
+    this._subscription.add(sub);
+  }
+
+  protected unregisterSubscription(sub: Subscription) {
+    this._subscription?.remove(sub);
+  }
+}