Skip to content

Instantly share code, notes, and snippets.

@rob3c
Last active June 12, 2024 09:17
Show Gist options
  • Save rob3c/c2c4dcc1116f94901ace179722c5f6d4 to your computer and use it in GitHub Desktop.
Save rob3c/c2c4dcc1116f94901ace179722c5f6d4 to your computer and use it in GitHub Desktop.
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 ngrx/store-devtools#33 and ngrx/store-devtools#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 [email protected]
        //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 zalmoxisus/redux-devtools-extension#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.

@jogboms
Copy link

jogboms commented Jun 19, 2017

Is it possible to have this same workaround for Nativescript?

@ciekawy
Copy link

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
Copy link

somq commented Oct 7, 2018

@Jonatthu
Copy link

@rob3c How about nativescript?

@shiv19
Copy link

shiv19 commented Jun 12, 2024

If any one else stumbles on this gist trying to make it work with nativescript, here is a poc I made a couple years ago
https://github.com/shiv19/nativescript-redux-poc

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