@fcannizzaro/streamdeck-react

Sharing State Across Actions

How to share state between isolated React roots using wrappers and external state managers.

Each action instance renders inside its own isolated React root. Local React state stays local to that action. To share state across actions, you need state that lives outside the component tree.

Wrapper API

@fcannizzaro/streamdeck-react supports wrapper components on both createPlugin and defineAction. These inject providers around action roots.

Plugin-Level Wrapper

Wraps all action roots:

import { createPlugin } from '@fcannizzaro/streamdeck-react';

const plugin = createPlugin({
  fonts: [...],
  actions: [...],
  wrapper: ({ children }) => <MyProvider>{children}</MyProvider>,
});

Action-Level Wrapper

Wraps a single action's root (nested inside the plugin wrapper):

import { defineAction } from "@fcannizzaro/streamdeck-react";

const action = defineAction({
  uuid: "com.example.action",
  key: MyKey,
  wrapper: ({ children }) => <MyActionProvider>{children}</MyActionProvider>,
});

Zustand

Zustand stores live in module scope, so each isolated React root subscribes to the same store directly. No wrapper needed.

// store.ts
import { create } from "zustand";

export const useCountStore = create<{ count: number; increment: () => void }>((set) => ({
  count: 0,
  increment: () => set((s) => ({ count: s.count + 1 })),
}));
// actions/increment.tsx
function IncrementKey() {
  const increment = useCountStore((s) => s.increment);

  useKeyDown(() => {
    increment();
  });

  // ...
}
// actions/display.tsx
function DisplayKey() {
  const count = useCountStore((s) => s.count);

  return (
    <div
      style={{
        width: "100%",
        height: "100%",
        alignItems: "center",
        justifyContent: "center",
        background: "#000",
      }}
    >
      <span style={{ color: "white", fontSize: 32 }}>{count}</span>
    </div>
  );
}

See samples/zustand/ for a full working example.

Jotai

Jotai atoms can be shared across roots when all roots use the same store. Provide a shared store through the plugin wrapper:

// store.ts
import { createStore } from "jotai";
export const store = createStore();
// wrapper.tsx
import { Provider } from "jotai";
import { store } from "./store";

export function JotaiWrapper({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}
// plugin.ts
const plugin = createPlugin({
  fonts: [...],
  actions: [...],
  wrapper: JotaiWrapper,
});

See samples/jotai/ for a full working example.

Action Coordinator (built-in)

For simple cross-action state sharing without external dependencies, use the built-in Action Coordinator. Enable it in createPlugin() and use useChannel() for shared state:

const plugin = createPlugin({
  coordinator: true,
  fonts: [...],
  actions: [...],
});
// In any action component:
const [volume, setVolume] = useChannel<number>("volume", 50);

The coordinator is ideal for sharing simple values (booleans, strings, numbers) between actions. For complex state management patterns (derived state, middleware, computed atoms), external stores like Zustand or Jotai remain the better choice.

See Action Coordinator for a full guide.

Built-in Settings Hooks

For simpler cases, use useSettings() for per-action persistence and useGlobalSettings() for plugin-wide persistence backed by the SDK. See Settings Hooks.

Decision Guide

NeedSolution
Simple per-action stateuseState / useReducer
Persist per-action settings across reloadsuseSettings<T>()
Plugin-wide shared configuseGlobalSettings<T>()
Simple cross-action state (built-in)useChannel() via Action Coordinator
Know which actions are visibleuseActionPresence() via Action Coordinator
Shared state across actions (no provider needed)Zustand store in module scope
Shared state with provider patternJotai/React Context via wrapper
Complex derived state / middlewareZustand or Jotai

On this page