← back

React Components on Stream Deck Hardware

2 min read

The official @elgato/streamdeck SDK is powerful but low-level. You track state manually, wire hardware events by hand, and generate key images yourself. Even a simple counter becomes a mix of event handlers, state bookkeeping, and rendering code. I wanted the same model I use in React apps — declare what the key looks like, let the framework handle the rest. So I built @fcannizzaro/streamdeck-react.

How it works

You write a React component for each action surface. defineAction() maps it to a manifest UUID. createPlugin() registers your actions and fonts, then plugin.connect() attaches to the Stream Deck runtime.

Each visible action instance on the hardware gets its own isolated React root — separate state, separate lifecycle. When state changes trigger a re-render, the library renders the JSX tree to an image and pushes it to the device via setImage(). Output is hashed so identical frames are never sent twice.

1st row: zustand state, 2rd row: tanstack query, react basic hooks

streamdeck-react in action

snake game in the Stream Deck+ LCD display

streamdeck-react in action

A counter example looks like this:

import { readFile } from 'node:fs/promises';
import { createPlugin, defineAction, useKeyDown, tw } from '@fcannizzaro/streamdeck-react';
import { useState } from 'react';

function CounterKey() {
  const [count, setCount] = useState(0);

  useKeyDown(() => setCount((c) => c + 1));

  return (
    <div
      className={tw(
        'flex h-full w-full flex-col items-center justify-center gap-1',
        'bg-linear-to-br from-[#0f172a] to-[#1d4ed8]',
      )}
    >
      <span className="text-[12px] font-semibold uppercase tracking-[0.2em] text-white/70">
        Count
      </span>
      <span className="text-[34px] font-black text-white">{count}</span>
    </div>
  );
}

const counterAction = defineAction({
  uuid: 'com.example.react-counter.counter',
  key: CounterKey,
});

const plugin = createPlugin({
  fonts: [
    {
      name: 'Inter',
      data: await readFile(new URL('../fonts/Inter-Regular.ttf', import.meta.url)),
      weight: 400,
      style: 'normal',
    },
  ],
  actions: [counterAction],
});

await plugin.connect();

What you get

  • Declarative rendering — describe keys as JSX, not imperative draw calls
  • Full React hooksuseState, useEffect, useRef, useContext, custom hooks all work as expected
  • Hardware-aware hooksuseKeyDown, useDialRotate, useTouchTap, settings hooks, lifecycle hooks, and SDK helpers compose with the rest of React
  • Gesture hooksuseTap, useLongPress, useDoubleTap for higher-level input handling on keys and touch surfaces
  • Built-in primitivesBox, Text, Image, Icon, ProgressBar, CircularGauge, and ErrorBoundary for compact device UIs
  • Flexible styling — inline styles, className, and a tw() helper for Tailwind-like utility strings
  • Encoder and dial support — separate key and dial components per action, with useDialHint for Stream Deck+ trigger descriptions
  • TouchBar component — render custom content on the Stream Deck+ touch display strip
  • Shared state — Zustand stores work out of the box, Jotai and others plug in through the wrapper API on createPlugin or defineAction
  • Output caching — FNV-1a hashing skips setImage() when the frame hasn’t changed
  • Error boundaries — every action root is wrapped automatically, one crash doesn’t take down the plugin
  • DevTools — browser-based inspector for debugging layouts and state during development
  • React Compiler — optional integration via Babel plugin to automatically optimize re-renders

Get started

bun create streamdeck-react

The CLI scaffolds a complete .sdPlugin project — manifest, bundler config (Rollup or Vite 8 with Rolldown), fonts, and a starter example. Pick from minimal, counter, Zustand, Jotai, or React Query templates.

For manual setup:

bun add @fcannizzaro/streamdeck-react react

Full documentation at streamdeckreact.fcannizzaro.com. Source on GitHub. Package on npm.