Catalog npm
PACKAGE EXPLORERView in registry

4b-react-mdns

npm

Discover mDNS/Bonjour servers on your LAN from a React overlay or CLI, and remember the one you pick.

bonjourclidiscoverydns-sdlanmdnsproviderreactzeroconf
Versions collected18
Direct dependencies9From release 1.8.0
Last observed20 Oct, 20:04 UTC

README

Current package documentation

4b-react-mdns

Let people pick a server off the local network from inside your React app, and remember which one they chose.

npm · source · changelog

Browsers can't speak mDNS — it's UDP multicast on :5353 — so discovery runs in Node and the React side consumes a small HTTP + Server-Sent-Events API. The package ships both halves: a Vite plugin (or standalone server) for the Node side, and a provider and hooks for the browser side.

Quick start

npm install 4b-react-mdns
npx 4bmdns config

config wires both halves — it adds mdnsPlugin() to your Vite config and wraps whatever your entry point renders in <MdnsProvider>, above every route — then asks whether the provider should also hold a live connection to the server you pick:

  ┌─ 4B App Configuration 1.5.0 • config ─────────────────────────────────┐
  │                                                                       │
  │  Configuring ~/code/my-app                                            │
  │                                                                       │
  │  ✔ vite config  vite.config.js    added mdnsPlugin({ httpOnly: true })│
  │  ✔ entry point  src/main.jsx      wrapped the rendered tree in        │
  │                                   <MdnsProvider httpOnly={true}       │
  │                                   launcher={false}>                   │
  │                                                                       │
  │  Live link to the picked server?                                      │
  │  ❯ None         just the picker — add one later with --ws or --socket │
  │    WebSocket    ws://<picked server>/ — reconnecting, no dependency   │
  │    Socket.IO    socket.io-client — you install it, we wire io()       │
  │    Both         a WebSocket and a Socket.IO connection                │
  │                                                                       │
  │  ✔ Done. Restart the dev server and the picker is live.               │
  └───────────────────────────────────────────────────────────────────────┘
                                          --- 4brains technologies (2026)

Nothing socket-related is written unless you pick it here or pass --ws / --socket. The question only appears in a real terminal; a pipe or CI run gets the result and no prompt. Changed your mind later? npx 4bmdns link switches it — see Switching later.

createRoot(document.getElementById('root')).render(
  <MdnsProvider httpOnly={true} launcher={false}>
    <StrictMode>
      <App />
    </StrictMode>
  </MdnsProvider>,
)

The two props are always written out with their values, so whoever reads the file later sees what was decided and can flip it. The defaults filter to web servers and skip the launcher pill: restart the dev server, press Space or double-tap the screen (or call useMdns().open() from your own button) and the picker opens. Pick a server and it's saved.

Flag
--http-only / --allhttpOnly={true} (default) / httpOnly={false}
--no-launcher / --launcherlauncher={false} (default) / launcher={true} — show the pill
--ws / --no-wsws={true} onWsMessage={pushRelay} plus a src/mdnsRelay.js helper / ws={false}
--socket / --no-socketsocket={true} socketClient={io} / socket={false} — Socket.IO
--dry-runShow the plan without writing
npx 4bmdns config --launcher --ws

Re-running is safe. A bare config leaves an existing setup alone; pass a flag and it updates that prop in place, keeping every other prop on the tag (baseUrl, onSelect, position …) and the formatting — a props-per-line tag stays props-per-line. If it doesn't recognise a file it leaves it alone and prints the snippet to paste rather than guessing.

Wiring it by hand instead
// vite.config.js
import { mdnsPlugin } from '4b-react-mdns/vite'

export default defineConfig({
  plugins: [react(), mdnsPlugin({ httpOnly: true })],
  server: { host: true }, // optional: reachable from a phone on the same Wi-Fi
})
// src/main.jsx
import { MdnsProvider } from '4b-react-mdns/react'

createRoot(document.getElementById('root')).render(
  <MdnsProvider httpOnly={true} launcher={false}>
    <App />
  </MdnsProvider>
)

Reading the pick

Anywhere inside the provider:

import { useMdns } from '4b-react-mdns/react'

function Player() {
  const { address, open } = useMdns()
  if (!address) return <button onClick={open}>Choose a server</button>
  return <video src={`http://${address}/stream`} />
}

Outside React — in a plain api client, say:

import { getSelectedAddress } from '4b-react-mdns/react'

const base = `http://${getSelectedAddress()}` // "192.168.0.224:3000"

The pick is stored in three places, and you can read any of them:

WhereWhat
localStorage["4b_mDNS_ip"]the plain ip:port string
localStorage["4b_mDNS_selection"]the full record as JSON
~/.scan-net/selection.jsonthe same record, on the machine running the scanner

Next to the pick the panel keeps one more flag, the Test / Live mode:

WhereWhat
localStorage["4b_App_Mode"]the string "true" (Live) or "false" (Test). Test is the default: the key is written as "false" the first time it is read

A value left under an earlier name (4b_mDNS_Mode or 4b_mDNS_live) is moved to 4b_App_Mode the first time it is read, and the old key is removed.

It is browser-only and never written to disk. The switch sits in the panel's status row beside the HTTP filter. Read it from a component with useMdns().live (a boolean) or useMdns().mode ("test" / "live"), or from a plain api module without the provider:

import { isLiveMode } from '4b-react-mdns/react'

const base = `http://${getSelectedAddress()}${isLiveMode() ? '/api' : '/api/test'}`
// or, with no import at all:
const live = localStorage.getItem('4b_App_Mode') === 'true'

The file is the durable copy — it survives a cleared browser, is shared by every app pointing at that scanner, and is readable from shell scripts and Node. Where the two disagree the file wins, with one exception: a scanner holding no file at all does not clear a pick the browser already remembers, since the stores are independent and an empty file is not a decision to forget anything. Forget in the panel (or clear()) empties the browser's copy.

When discovery can't see the server

mDNS only reaches the local subnet, and some networks block multicast outright. The Enter the server address field at the top of the panel covers that case: type 192.168.0.10:3000 (or a hostname, a bracketed IPv6 literal, or a full URL — the scheme and path are dropped) and press Use this address. It is stored exactly like a pick from the list, so localStorage["4b_mDNS_ip"] and the JSON file both update, and useMdns().address changes the same way. The record carries manual: true so you can tell it apart.

Saving is browser-first: the pick is written to localStorage before the API is asked to persist it, so it survives the scanner being unreachable. In that case the panel keeps the browser copy and shows a banner saying the disk copy is missing.

Hide the box with <ScannerPanel showManualEntry={false}>. To build your own form, use the pieces:

import { ManualEntry, parseAddress, manualService, useMdns } from '4b-react-mdns/react'

const { select } = useMdns()
const parsed = parseAddress('http://nas.local:8080/admin') // { host: 'nas.local', port: 8080 }
if (!parsed.error) select(manualService(parsed.host, parsed.port))

<MdnsProvider>

Wraps your app and layers the picker over it. Children render untouched — the provider adds no wrapper element. The panel is portalled to <body> at z-index: 2147483000, so it sits above routed content no matter where the provider is mounted or what stacking contexts your app creates. Styles are injected at runtime and every selector is scoped under .snet-root, so there's no stylesheet to import and nothing bleeds either way.

Everything inside the panel is sized in em off a single custom property, so the whole thing scales as one piece. --snet-fs is flat at 15px up to roughly 1920px of viewport width and grows beyond it — a 4K screen at 100% scaling gets a panel sized for it instead of a postage stamp. Pin it to opt out:

.snet-root { --snet-fs: 15px }   /* or whatever size you want the panel to sit at */
PropDefault
baseUrl/apiWhere the scanner API is mounted
httpOnlyserver defaulttrue = only _http/_https._tcp, false = every service. Omit to follow the CLI's --http-only flag
filterTogglefalse when httpOnly is setShow the Web servers only switch in the panel
launchertrueShow the built-in launcher pill. false removes it entirely
launcherLabelScan networkIts text
positionbottom-rightIts corner — also bottom-left, top-right, top-left
open—Pass a boolean to control visibility yourself
onOpenChange—Called with true/false when the user opens or dismisses the panel
defaultOpenfalseStarting state when open isn't passed
closeOnEscape / closeOnBackdroptrueDismiss behaviour
hotkey" " (Space)Key that toggles the panel when focus isn't in a field or on a button. Any KeyboardEvent.key value, or false to disable
doubleTaptrueOpen the panel on a double tap (or double click) anywhere outside a field, button or link. false to disable, or { delay, slop } to tune the ms allowed between taps (350) and the px they may drift (30)
onSelect—Called with the record whenever the pick changes
modeToggletrueShow the Test / Live switch in the panel. false hides it; the saved flag still applies
onModeChange—Called with (mode, live) — ("live", true) or ("test", false) — each time the switch is flipped
disconnectNoticetruePop up a notice in the launcher's corner when a socket drops, with Retry and Check server buttons. false to turn it off
wsfalseKeep a reconnecting WebSocket open to the picked server. true, or an options object — see Sockets
wsPath/Path the WebSocket dials on the pick, e.g. /live
wsUrl—Dial this instead: a string, or (selection) => url
wsProtocols—Sec-WebSocket-Protocol, a string or array
wsReconnecttruefalse to reconnect never, or { maxAttempts, baseDelay, maxDelay, factor, jitter }
wsDecodeautoauto parses JSON when it is JSON, json always tries, raw leaves the string alone
wsLastMessagetrueKeep ws.lastMessage up to date. false for a chatty server: messages then reach ws.on(), onWsMessage and the relay only, and nothing re-renders per message
onWsMessage / onWsStatus / onWsOpen / onWsClose / onWsError—WebSocket callbacks. onWsMessage(data, message, event); onWsStatus(status, snapshot)
socketfalseKeep a Socket.IO connection open to the picked server. true, or an options object
socketClientwindow.ioThe io export of socket.io-client. The library is not bundled
socketPath/socket.ioSocket.IO path on the server
socketUrl—Dial this instead: a string, or (selection) => url
socketOptions—Passed to io(url, options) — transports, auth, reconnectionAttempts …
socketLastEventtrueKeep socket.lastEvent up to date. false for a busy server; events then reach socket.on() and onSocketEvent only
onSocketStatus / onSocketEvent / onSocketConnect / onSocketDisconnect—Socket.IO callbacks. onSocketEvent(event, ...args) fires for every server event

useMdns()

const {
  selection, ip, address, url,   // the pick
  select, clear,                 // change it
  services, connected, rescan,   // the scan
  httpOnly, setHttpOnly,         // the filter
  mode, live, setMode, setLive,  // Test / Live — "test" | "live", and the boolean behind it
  open, close, toggle, isOpen,   // the panel
  ws, socket,                    // sockets to the pick — see below
} = useMdns()

Your own button

Drop the built-in pill and open the panel from anywhere:

function ServerButton() {
  const { open, address } = useMdns()
  return <button onClick={open}>{address ?? 'Pick a server'}</button>
}

<MdnsProvider launcher={false}>
  <Header><ServerButton /></Header>
  <Routes />
</MdnsProvider>

To hold the state yourself — driving it from a router, a menu, a keyboard shortcut — pass open and it becomes a controlled component:

const [scanning, setScanning] = useState(false)

<MdnsProvider launcher={false} open={scanning} onOpenChange={setScanning} httpOnly>
  <button onClick={() => setScanning(true)}>Scan network</button>
  <Routes />
</MdnsProvider>

While open is a boolean the provider never changes it on its own: Escape, the ✕ and a backdrop click all report through onOpenChange and leave the decision to you. Omit open and it manages itself again.

<MdnsList>

Renders the scanned services on your own page instead of behind the launcher:

<MdnsProvider httpOnly>
  <h2>Pick a streaming server</h2>
  <MdnsList />
</MdnsProvider>
PropDefault
httpOnlyprovider's valueOverride the filter for this list only
query''Substring filter over name, type, host, addresses
limit—Show at most this many
columnstruefalse renders a single column
emptyLabel—Text when nothing matches
onSelect—Called after a card is picked

Selecting from the list saves exactly as the overlay does. For full control over the markup, read useMdns().services and render the rows yourself.

Sockets to the picked server

Most apps that pick a LAN server go on to hold a live connection to it, and a browser WebSocket closes once and stays closed — a server restart or a phone changing Wi-Fi leaves the app talking to nothing. The provider can own that connection for you. Both kinds are off by default; turn one on and it dials the pick, follows it when the pick changes, reconnects when it drops, and shows its status in the panel and as an extra dot on the launcher.

<MdnsProvider ws={true}>            {/* ws://<address>/ */}
<MdnsProvider ws={{ path: '/live', reconnect: { maxAttempts: 10, maxDelay: 5000 } }}>
function Readings() {
  const { ws } = useMdns()
  // 'off' | 'connecting' | 'open' | 'reconnecting' | 'closed' | 'error'
  if (ws.status !== 'open') return <p>{ws.status}{ws.attempt ? ` (attempt ${ws.attempt})` : ''}</p>
  return (
    <>
      <pre>{JSON.stringify(ws.lastMessage?.data)}</pre>
      <button onClick={() => ws.send({ cmd: 'refresh' })}>Refresh</button>
    </>
  )
}

ws carries status, connected, attempt, error, url, lastMessage ({ data, raw, at }, with data already parsed when the server sent JSON), nextRetryAt, queued, and the actions send, on, connect, disconnect, reconnect, getSocket. send() takes an object and JSON-encodes it, or a string / ArrayBuffer / Blob as-is; while the link is down the message waits in a small outbox and goes out on open. disconnect() closes on purpose — no retries until connect(). Reconnection is exponential back-off from baseDelay to maxDelay with jitter, restarted the moment the browser fires online. The scheme follows the pick: an _https._tcp service gets wss://. (A page served over https:// cannot open a plain ws:// socket — browsers block mixed content — so serve the app over http:// on the LAN or give the server TLS.)

Socket.IO is the same shape. The client library is yours to install, since bundling it here would force it on apps that use plain WebSockets:

npm install socket.io-client
npx 4bmdns config --socket     # writes socket={true} socketClient={io} and the import
import { io } from 'socket.io-client'

<MdnsProvider socket={true} socketClient={io} socketOptions={{ auth: { token } }}>
function Room() {
  const { socket } = useMdns()
  const [state, setState] = useState()
  // on() returns the unsubscribe, so it drops straight into useEffect.
  useEffect(() => socket.on('state', setState), [socket.on, socket.status])
  return <button onClick={() => socket.emit('join', 'lobby')}>{socket.status}</button>
}

socket carries status, connected, attempt, error, id, url, lastEvent ({ event, args, at }) and the actions emit, on, connect, disconnect, reconnect, getSocket. Socket.IO runs its own reconnection; the provider mirrors its connect, disconnect, connect_error and reconnect_attempt events into the same status words the WebSocket uses, so one status component serves both. Without a client it reports status: 'error' and says what to pass.

Listening and sending

Both connections live in the provider, so any component reads them from useMdns() — no prop drilling, no second connection. Here is a component that shows live readings over the WebSocket and sends commands back:

import { useEffect, useState } from 'react'
import { useMdns } from '4b-react-mdns/react'

function Readings() {
  const { ws } = useMdns()
  const [reading, setReading] = useState(null)

  // Every message, as it arrives. `data` is already parsed when the server sent JSON.
  useEffect(() => {
    if (ws.lastMessage) setReading(ws.lastMessage.data)
  }, [ws.lastMessage])

  const refresh = () => ws.send({ cmd: 'refresh' })          // objects are JSON-encoded
  const raw = () => ws.send('PING')                          // strings go through as-is

  return (
    <section>
      <p>
        {ws.status === 'open' && '● live'}
        {ws.status === 'reconnecting' && `● reconnecting (attempt ${ws.attempt})`}
        {ws.status === 'error' && `● ${ws.error}`}
        {ws.status === 'off' && '○ pick a server first'}
      </p>
      <pre>{JSON.stringify(reading, null, 2)}</pre>
      <button onClick={refresh} disabled={!ws.connected}>Refresh</button>
      <button onClick={raw}>Ping</button>
    </section>
  )
}

Reading ws.lastMessage re-renders on every message — and because it lives in the provider's context, so does every component that calls useMdns(), whether it reads it or not. That is fine for a reading a few times a second and wrong for a server that streams sensor frames or cursor positions. For those, turn the tracking off and subscribe instead — no re-render anywhere, and the subscription survives reconnects and a change of pick:

<MdnsProvider ws={true} wsLastMessage={false}>
function Cursor() {
  const { ws } = useMdns()
  const pos = useRef({ x: 0, y: 0 })
  // on() returns the unsubscribe, so it drops straight into useEffect.
  useEffect(() => ws.on((data) => { pos.current = data }), [ws.on])
  ...
}

config --ws scaffolds the same idea as a file, src/mdnsRelay.js, and wires it with onWsMessage={pushRelay} on the provider. Any module — React or not — can then listen:

import { onRelay } from './mdnsRelay'

useEffect(() => onRelay((data) => handle(data)), [])

The file is generated once and never rewritten, so it is yours to edit. Delete the prop and the import if you would rather use ws.on() or onWsMessage directly.

The same with Socket.IO — named events in, named events out:

function Room({ name }) {
  const { socket } = useMdns()
  const [members, setMembers] = useState([])
  const [chat, setChat] = useState([])

  // socket.on() returns the unsubscribe, so it drops straight into useEffect.
  // Re-run on status so a fresh connection is subscribed again.
  useEffect(() => socket.on('members', setMembers), [socket.on, socket.status])
  useEffect(
    () => socket.on('chat', (msg) => setChat((c) => [...c, msg])),
    [socket.on, socket.status]
  )

  // Join when connected — and again after every reconnect, since the server forgot us.
  useEffect(() => {
    if (socket.connected) socket.emit('join', name)
  }, [socket.connected, name])

  const say = (text) => socket.emit('chat', { name, text })

  return (
    <section>
      <p>{socket.connected ? `● in ${name} as ${socket.id}` : `○ ${socket.status}`}</p>
      <ul>{members.map((m) => <li key={m}>{m}</li>)}</ul>
      <ul>{chat.map((m, i) => <li key={i}><b>{m.name}</b> {m.text}</li>)}</ul>
      <button onClick={() => say('hi')}>Say hi</button>
    </section>
  )
}

socket.emit() takes the same arguments as Socket.IO's own, acknowledgements included:

socket.emit('save', payload, (ack) => console.log('server said', ack))

For anything the wrappers don't cover, ws.getSocket() returns the browser WebSocket and socket.getSocket() the Socket.IO Socket — the current one, so read it when you need it rather than holding on to it across a reconnect.

Switching later

Wired Socket.IO and now want a WebSocket, or the other way round? Run link in the app:

npx 4bmdns link ws        # Socket.IO → WebSocket
npx 4bmdns link socket    # WebSocket → Socket.IO
npx 4bmdns link both      # keep both
npx 4bmdns link none      # picker only
npx 4bmdns link           # a menu, with the current setup marked
  ┌─ 4B App Configuration 1.5.0 • link ───────────────────────────────────┐
  │                                                                       │
  │  Live link in ~/code/my-app                                           │
  │                                                                       │
  │  Now: Socket.IO  src/main.jsx                                         │
  │                                                                       │
  │  ✔ entry point  src/main.jsx      set ws={true} socket={false} on     │
  │                                   <MdnsProvider>                      │
  │                                                                       │
  │  ✔ Switched Socket.IO ❯ WebSocket. Restart the dev server.            │
  └───────────────────────────────────────────────────────────────────────┘
                                          --- 4brains technologies (2026)

It edits only the ws and socket props on the tag and leaves everything else — baseUrl, onSelect, your callbacks — as it was. Turning Socket.IO off also removes the socketClient={io} prop and the import { io } from 'socket.io-client' line it came with (only if nothing else in the file uses io), so you can npm uninstall socket.io-client without a dangling import. Turning the WebSocket off likewise removes onWsMessage={pushRelay} and its import; the mdnsRelay file stays, since you may have edited it. An onWsMessage you wrote yourself is never touched. The flags config --ws / --socket still work but are additive; link replaces.

When the connection drops

A socket that was up and is now retrying pops a small notice in the launcher's corner:

WebSocket disconnected Trying to reconnect — attempt 3. Check the server is running. ws://192.168.0.10:3000/ [Retry now] [Check server]

Retry now skips the back-off and dials immediately. Check server opens the picker, where the connections row shows the same status and you can pick another server or rescan. The notice dismisses itself when the link is back, after a short green "reconnected" flash, or when you close it for this outage. A link that never connected says "can't reach the server" instead. It stays out of the way while the picker is open, and disconnectNotice={false} turns it off entirely if you show the status your own way.

The pieces are exported for use on their own, inside or outside the provider:

import { useWebSocket, useSocketIo, ReconnectingSocket, linkUrl } from '4b-react-mdns/react'

const link = useWebSocket('ws://192.168.0.10:3000/live', { onMessage: setReading })
const sio = useSocketIo(linkUrl(selection, { scheme: 'http', path: '' }), { client: io })

// No React: the class the hook is built on.
const rs = new ReconnectingSocket('ws://192.168.0.10:3000/')
rs.on('message', ({ data }) => console.log(data))
rs.connect()

Filtering to web servers

Most networks are noisy — printers, AirPlay targets, every laptop. httpOnly narrows the list to _http._tcp and _https._tcp:

mDNS: advertising "streaming-server" at Mac.local:3000 (_http._tcp.local)
mDNS: advertising "4bnode-app (4brains)" at 4brains.local:4021 (_http._tcp.local)

It's settable at every level, each overriding the one above:

npx 4bmdns --http-only              # the server default
<MdnsProvider httpOnly={true}>      {/* the app's default */}
<MdnsList httpOnly={false} />       {/* just this list */}

The Web servers only toggle in the panel flips it live. The UI streams every service and filters in the browser, which is what lets the toggle switch both ways instantly.

Passing httpOnly hides that toggle — the app has decided, so offering a switch that contradicts it only invites confusion. Pass filterToggle to bring it back:

<MdnsProvider httpOnly>                      {/* filtered, no toggle */}
<MdnsProvider httpOnly filterToggle>         {/* filtered, toggle shown */}
<MdnsProvider>                               {/* server default, toggle shown */}

Serving the API

The provider needs the scanner API on baseUrl. Three ways to get it there.

The Vite plugin covers npm run dev and npm run preview:

import { mdnsPlugin } from '4b-react-mdns/vite'

export default defineConfig({ plugins: [react(), mdnsPlugin({ httpOnly: true })] })

A separate process — for production, or when your app already serves its own /api/* and the routes would collide:

npx 4bmdns -p 4173
<MdnsProvider baseUrl="http://localhost:4173/api">

Embedded in your own server:

import { createServer } from '4b-react-mdns'

const { urls, storeFile, close } = await createServer({ port: 4173, httpOnly: true })

If the API isn't reachable the panel says so — a 404 reports that the plugin is missing rather than pretending the connection dropped.

CLI

4bmdns and scan-net are the same command.

npx 4bmdns              # start the scanner UI on :4173
npx 4bmdns config       # wire the scanner into the app in this directory
npx 4bmdns link ws      # switch the live link: ws, socket, both or none
npx 4bmdns which        # print the selected server
npx 4bmdns forget       # clear it
$ 4bmdns which
streaming-server  ->  192.168.0.224:3000
http://192.168.0.224:3000
FlagDefault
-p, --port <number>4173Port to listen on
-H, --host <host>0.0.0.0Interface to bind
--http-onlyoffList only _http._tcp / _https._tcp
--allonList every advertised service
--store <dir>~/.scan-netWhere the selection is saved
--dry-run—For config: show the plan without writing
--launcher / --no-launcheroffFor config: write launcher={true} / launcher={false}
--ws / --no-ws—For config: write ws={true} / ws={false}
--socket / --no-socket—For config: write socket={true} socketClient={io} / socket={false}
--json—Machine-readable output for which
--no-open—Don't open a browser

SCAN_NET_HOME overrides the store directory too, so you can scope a selection per project.

Node API

import { SelectionStore, createServer, MdnsScanner } from '4b-react-mdns'

const { ip, port, url } = new SelectionStore().read() ?? {}

selection.json looks like this:

{
  "id": "Living Room Pi._http._tcp.local",
  "name": "Living Room Pi",
  "kind": "Web server",
  "type": "http",
  "protocol": "tcp",
  "host": "livingroom.local",
  "ip": "192.168.1.42",
  "port": 8080,
  "address": "192.168.1.42:8080",
  "url": "http://livingroom.local:8080",
  "txt": null,
  "savedAt": "2026-09-09T10:31:07.482Z"
}

ip prefers IPv4, falls back to IPv6, then to the advertised .local hostname.

HTTP API

Endpoint
GET /api/status{ scanning, count, startedAt }
GET /api/config{ httpOnly } — the server's default filter
GET /api/servicesservices discovered so far; ?httpOnly= overrides the default
GET /api/streamSSE: snapshot, then up / update / down; ?httpOnly= too
POST /api/scanstart scanning, or re-query to refresh
POST /api/stopstop scanning and drop results
GET /api/selection{ selection, file }
POST /api/selectionsave a service as the selection
DELETE /api/selectionclear it

Using it from another device

With server: { host: true } (or the standalone CLI, which binds 0.0.0.0), the UI works from a phone or laptop on the same Wi-Fi at http://<your-ip>:5173. Selecting there writes to the JSON file on the machine running the scanner, which is usually what you want.

One caveat: a plain-HTTP LAN origin isn't a secure context, so browsers hide the async clipboard API entirely. Copy address falls back to a legacy copy, and if that's blocked too it highlights the address so you can copy it manually. Clicking the address selects it as well.

There's no authentication on the API, so run it on networks you trust — or bind the standalone server to loopback with -H 127.0.0.1.

Requirements

Node 20+. React 18 or 19 as a peer dependency, needed only for the /react entry. socket.io-client 4+ is an optional peer, needed only when socket is on.

Develop

git clone https://git.4brains.in/shailesh.gautam/react-mdns-package.git
cd react-mdns-package
npm install
npm run dev      # Vite + the mDNS API on :5173
npm run build    # emit dist/ (standalone UI) and dist/react/ (the library)
npm start        # serve the built UI the way the published package does
npm run lint     # oxlint

npm run build runs twice: once for the standalone page in dist/, once for the embeddable library in dist/react/ with React left external. prepack runs it automatically, so npm publish always ships a current build.

To try unreleased changes in a host app, link the working copy and rebuild after each edit — Vite won't hot-reload a linked dependency's server code:

cd ../your-app && npm install ../react-mdns-package

Layout

server/scanner, HTTP API, Vite plugin, standalone server, config codemod
src/lib/the embeddable React entry — provider, hooks, panel, styles
src/the standalone page served by the CLI
bin/cli.js4bmdns / scan-net

License

MIT

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 18

Collected releases
VersionPublishedDirect dependenciesOpen version
1.8.0Registry latest6 Oct 20269
1.7.029 Sept 20269
1.6.129 Sept 20269
1.5.029 Sept 20269
1.4.828 Sept 20268
1.4.428 Sept 20268
1.1.028 Sept 20268
0.8.119 Sept 20268
0.8.012 Sept 20268
0.7.210 Sept 20268
0.7.110 Sept 20268
0.7.010 Sept 20268
0.6.010 Sept 20268
0.5.010 Sept 20268
0.4.010 Sept 20268
0.3.010 Sept 20268
0.2.010 Sept 20268
0.1.09 Sept 20268

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 20:04 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:e8bce870cc5855443f90facc7eddc924b7e7bad321fadbdd09a6dd8a148f7135