Instantly share code, notes, and snippets.

Embed
What would you like to do?
Using @ngrx/store-devtools remotely with Ionic 2

Ok, I have on-device remote store debugging working with Ionic 2. Unfortunately, time-travel and state import doesn't work with store-devtools yet (see https://github.com/ngrx/store-devtools/issues/33 and https://github.com/ngrx/store-devtools/issues/31), but at least the Inspector, Log Monitor and Graph is working remotely. Here's how:

First, add https://github.com/zalmoxisus/remotedev to the project:

> npm install --save-dev remotedev

Then add these devtools proxy wrapper classes to the project. They provide the same interface as the browser extension so store-devtools will think it's just talking to the chrome extension. I left in the trace debug logging so you can clearly see what's happening in the console, but it's easy to remove if you want.

remote-devtools-proxy.ts

import moment from 'moment';
import {
    ReduxDevtoolsExtension,
    ReduxDevtoolsExtensionConnection
} from '@ngrx/store-devtools/src/extension';
import { connect, extractState } from 'remotedev/lib/devTools';

export class RemoteDevToolsConnectionProxy implements ReduxDevtoolsExtensionConnection {
    className = 'RemoteDevToolsConnectionProxy';

    constructor(
        public remotedev: any,
        public instanceId: string
    ) {
    }

    subscribe(
        listener: (change: any) => void
    ): any {
        const logContext = `${this.className} - subscribe`;
        console.log(`[${logContext}] listener: ${listener.toString()}`);

        const listenerWrapper = (change: any) => {
            console.log(`[${logContext} - listenerWrapper] change: ${
                JSON.stringify(change, null, 4)}`);

            // extractState handles circular references, unlike JSON.parse directly. See:
            // https://github.com/zalmoxisus/remotedev/issues/5#issuecomment-257009656
            const state = extractState(change);
            console.log(`[${logContext} - listenerWrapper] parsed state: ${
                JSON.stringify(state, null, 4)}`);

            // WARNING: @ngrx/store-devtools does NOT handle the time-travel changes
            // correctly, so this doesn't actually work yet! See the following issues:
            // https://github.com/ngrx/store-devtools/issues/33
            // https://github.com/ngrx/store-devtools/issues/31
            listener(change);
        };

        this.remotedev.subscribe(listenerWrapper);
    }

    unsubscribe(
    ): any {
        // HACK a bug in remotedev ignores the real instanceId. See:
        // https://github.com/zalmoxisus/remotedev/issues/4
        // UPDATE: fixed - quickly! - in remotedev@0.2.3
        //const instanceId = 0; // internal array index instead of actual this.instanceId;
        const instanceId = this.instanceId;
        console.log(`[${this.className} - unsubscribe] instanceId: ${instanceId}`);

        // HACK fix bug in @ngrx/store-devtools that calls this instead of returning
        // a lambda that calls it when their Observable wrapper is unsubscribed.
        return () => this.remotedev.unsubscribe(instanceId);
    }

    // NOTE: THIS IS NEVER CALLED - see send() on RemoteDevToolsProxy below
    send(
        action: any,
        state: any
    ): any {
        console.log(`[${this.className} -send]\n` +
            `action: ${JSON.stringify(action, null, 4)},\n` +
            `state: ${JSON.stringify(state, null, 4)}`);

        this.remotedev.send(action, state);
    }
}

export class RemoteDevToolsProxy implements ReduxDevtoolsExtension {
    className = 'RemoteDevToolsProxy';
    remotedev: any = null;
    defaultOptions = {
        realtime: true,
        hostname: 'localhost',
        port: 8000,
        autoReconnect: true,
        connectTimeout: 20000,
        ackTimeout: 10000,
        secure: true,
    };

    constructor(
        defaultOptions: Object
    ) {
        this.defaultOptions = Object.assign(this.defaultOptions, defaultOptions);
    }

    connect(
        options: {
            shouldStringify?: boolean;
            instanceId: string;
        }
    ): ReduxDevtoolsExtensionConnection {
        const logContext = `${this.className} - connect`;
        console.log(`[${logContext}] options: ${JSON.stringify(options, null, 4)}`);

        const connectOptions = Object.assign(this.defaultOptions, options);
        console.log(`[redux extension - connect] connectOptions: ${
            JSON.stringify(connectOptions, null, 4)}`);

        this.remotedev = connect(connectOptions);
        console.log(`[${logContext}] remotedev:`);
        console.log(this.remotedev);

        const connectionProxy = new RemoteDevToolsConnectionProxy(
            this.remotedev, connectOptions.instanceId);

        return connectionProxy;
    }

    send(
        action: any,
        state: any,
        shouldStringify?: boolean,
        instanceId?: string
    ): any {
        console.log(`[${this.className} - send]\n` +
            `action: ${JSON.stringify(action, null, 4)},\n` +
            `state: ${JSON.stringify(state, null, 4)},\n` +
            `shouldStringify: ${shouldStringify}, instanceId: ${instanceId}`
        );

        this.remotedev.send(action, state);
    }
}

Then in your app.module.ts, just add an instance to window.devToolsExtension (legacy) and/or window.__REDUX_DEVTOOLS_EXTENSION__ (current) if you're not in a browser where it already exists (see deprecation of window.devToolsExtension in https://github.com/zalmoxisus/redux-devtools-extension/issues/220). Then call StoreDevtoolsModule.instrumentOnlyWithExtension(), as usual.

app.module.ts (excerpt)

// ...
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { RemoteDevToolsProxy } from './remote-devtools-proxy'; // our new wrapper class
// ...

// Register our remote devtools if we're on-device and not in a browser
if (!window['devToolsExtension'] && !window['__REDUX_DEVTOOLS_EXTENSION__']) {
    let remoteDevToolsProxy = new RemoteDevToolsProxy({
        connectTimeout: 300000, // extend for pauses during debugging
        ackTimeout: 120000,  // extend for pauses during debugging
        secure: false, // dev only
    });

    // support both the legacy and new keys, for now
    window['devToolsExtension'] = remoteDevToolsProxy;
    window['__REDUX_DEVTOOLS_EXTENSION__'] = remoteDevToolsProxy;
}
// ...
@NgModule({
    imports: [
        // ...
        // the devtools looks for a window.devToolsExtension to attach to,
        // which we registered above if there wasn't one already.
        StoreDevtoolsModule.instrumentOnlyWithExtension(),
        // ...
    ],
    // ...
})
export class AppModule {}

Whew, ok now that we have the remote client set up, let's run a server for it to talk to. I just installed remotedev-server globally and ran it like this:

> npm install -g remotedev-server
> remotedev --hostname=my.local.ip.address --port=8000

Make sure the hostname matches what was configured in app.module.ts. The default is localhost. Once that's up and listening for connections, we can use the Chrome Redux DevTools extension to connect to the remotedev server via the Remote button in the interface.

Now, run the app on the device. For an Android device connected via USB, I use Chrome's port forwarding feature to forward port 8000 to my dev box.

@jrmcdona

This comment has been minimized.

jrmcdona commented May 11, 2017

Hi, do you still have to use this as a workaround?
Are you using this with OSX and running iOS? I am using Inonic and trying to debug my ngrx state.

@jogboms

This comment has been minimized.

jogboms commented Jun 19, 2017

Is it possible to have this same workaround for Nativescript?

@ciekawy

This comment has been minimized.

ciekawy commented Apr 27, 2018

thats awesome! - just managed to do it on macos / iOS using ngrx 4.4.1 and ionic 4.3
the only modification - I use IP address suggested by ionic cordova run ios --device --livereload --debug (for both RemoteDevToolsProxy and remotedev --hostname)

@somq

This comment has been minimized.

somq commented Oct 7, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment