Socket
Book a DemoInstallSign in
Socket

@rn-devtools/plugin-sdk

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@rn-devtools/plugin-sdk

Plugin SDK for rn-devtools

latest
Source
npmnpm
Version
1.0.3
Version published
Maintainers
1
Created
Source

@rn-devtools/plugin-sdk

Tools and utilities for building rn-devtools plugins on both the dashboard (web) and the device (native) sides.

Installation

# with yarn
yarn add -D @rn-devtools/plugin-sdk

# or with npm
npm i -D @rn-devtools/plugin-sdk

Peer dependency: socket.io-client v4 is expected in apps that use this SDK.

Quick Start

1) Pick a pluginId

Use a stable string, e.g. "rn-devtools.demo.counter".

2) Native (device) side

Create a client bound to your pluginId, the active socket, and the current deviceId.

import type { Socket } from 'socket.io-client';
import { createNativePluginClient } from '@rn-devtools/plugin-sdk';

// somewhere in your native runtime
function makeNativeBus(socket: Socket, deviceId: string) {
  const pluginId = 'rn-devtools.demo.counter';
  const bus = createNativePluginClient(pluginId, socket, deviceId);

  // send an event up to the dashboard
  bus.sendMessage('counter:value', { value: 42 });

  // listen for commands coming down from the dashboard
  const unsubscribe = bus.addMessageListener<{ step: number }>('counter:inc', ({ step }) => {
    // increment your native counter by `step`
  });

  // later, on cleanup
  return () => unsubscribe();
}

3) Dashboard (web) side

Create a client for the same pluginId. Provide a function that returns the currently selected device id.

import * as React from 'react';
import { createWebPluginClient } from '@rn-devtools/plugin-sdk';

export function CounterPanel({ currentDeviceId }: { currentDeviceId?: string }) {
  const pluginId = 'rn-devtools.demo.counter';
  const bus = React.useMemo(
    () => createWebPluginClient(pluginId, () => currentDeviceId),
    [pluginId, currentDeviceId]
  );

  React.useEffect(() => {
    // listen for values coming from the device
    const unsub = bus.addMessageListener<{ value: number }>('counter:value', (payload, { deviceId }) => {
      console.log('value from', deviceId, payload.value);
    });
    return () => unsub();
  }, [bus]);

  return (
    <button onClick={() => bus.sendMessage('counter:inc', { step: 1 })}>
      Increment on device
    </button>
  );
}

Message Flow

Dashboard (web)                           Device (native)
------------------------------------     ------------------------------------
sendMessage(event, payload, deviceId)  →  socket.emit("plugin:down", { pluginId, deviceId, event, payload })
                                          ↑
socket.on("plugin:up", handler)       ←   sendMessage(event, payload)
                                          socket.emit("plugin:up", { pluginId, deviceId, event, payload, timestamp })
  • On dashboard init, a singleton socket is created and says hello:

    • socket = io('/', { transports: ['websocket', 'polling'] })
    • socket.emit('devtools:hello', { role: 'dashboard' })

API Reference

createNativePluginClient(pluginId, socket, deviceId) => NativeBus

Builds a message bus bound to a specific device and plugin.

export type NativeBus = {
  sendMessage: (event: string, payload?: unknown) => void;
  addMessageListener: <T = unknown>(event: string, cb: (payload: T) => void) => () => void;
};
  • sendMessage(event, payload?) → emits plugin:up with { pluginId, deviceId, event, payload, timestamp }.
  • addMessageListener(event, cb) → listens to plugin:down and filters by { pluginId, deviceId, event }. Returns an unsubscribe function.

createWebPluginClient(pluginId, getDeviceId) => WebBus

Creates or reuses a singleton socket and builds a bus scoped to your plugin.

export type WebBus = {
  socket: Socket; // shared across plugins
  sendMessage: (event: string, payload?: unknown, deviceId?: string) => void;
  addMessageListener: <T = unknown>(
    event: string,
    cb: (payload: T, meta: { deviceId: string }) => void,
  ) => () => void;
};
  • sendMessage(event, payload?, deviceId?) → emits plugin:down to the resolved device id (deviceId ?? getDeviceId()); no-op if no device id is available.
  • addMessageListener(event, cb) → listens to plugin:up for your pluginId and event. The callback receives { payload, meta: { deviceId } }. Returns an unsubscribe function.

Note: The dashboard socket is a singleton. All plugins share the same io('/') connection.

Shared Types

export type Device = {
  id: string;
  deviceId: string;
  deviceName: string;
  isConnected: boolean;
  platform?: string;
};

export type PluginProps = {
  targetDevice: Device;      // the currently focused device
  allDevices: Device[];      // every known device
  isDashboardConnected: boolean;
};

export type PluginMsg = {
  pluginId: string;
  deviceId?: string;
  event: string;
  payload?: Record<string, unknown>;
  timestamp?: number; // set by native on plugin:up
};

export type DevtoolsPlugin = {
  id: string; // your pluginId
  title: string; // display name
  Icon: React.FC<{ className?: string }>;
  mount: React.ComponentType<PluginProps>; // the main React view of your plugin
};

export type NativeHookProps = {
  socket: Socket;
  deviceId: string;
};

Example: minimal DevtoolsPlugin

import type { DevtoolsPlugin, PluginProps } from '@rn-devtools/plugin-sdk';
import { createWebPluginClient } from '@rn-devtools/plugin-sdk';

function Panel(props: PluginProps) {
  const bus = React.useMemo(
    () => createWebPluginClient('rn-devtools.demo.counter', () => props.targetDevice?.deviceId),
    [props.targetDevice?.deviceId]
  );

  // ...render UI and talk to native via `bus`
  return null;
}

export const CounterPlugin: DevtoolsPlugin = {
  id: 'rn-devtools.demo.counter',
  title: 'Counter',
  Icon: (p) => <svg {...p} viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>,
  mount: Panel,
};

Events & Conventions

  • Upstream (native → web): plugin:up with { pluginId, deviceId, event, payload, timestamp }
  • Downstream (web → native): plugin:down with { pluginId, deviceId, event, payload }
  • Handshake (web only): devtools:hello with { role: 'dashboard' } emitted once per session
  • Unsubscribe early: always call the function returned by addMessageListener in useEffect cleanup or component teardown

Gotchas & Notes

  • The dashboard client ignores sends if it cannot resolve a device id. Make sure getDeviceId() is stable and returns a value when your UI is active.

  • The native client filters by exact pluginId, deviceId, and event. Make sure they match on both ends.

  • timestamp is added on native plugin:up messages; the dashboard does not set it.

  • If you want to support broadcasts from native (i.e., deviceId omitted), confirm your filtering logic. A common pattern is:

    // Accept messages for the current device *or* broadcasts
    if (msg.deviceId !== undefined && msg.deviceId !== current) return;
    

TypeScript Tips

  • Use generics on addMessageListener<T>() to get typed payloads per event.
  • Consider building a union of event→payload mappings for your plugin and wrapping sendMessage/addMessageListener to constrain event names.

Keywords

react native devtools

FAQs

Package last updated on 20 Aug 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts