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
| Need | Solution |
|---|---|
| Simple per-action state | useState / useReducer |
| Persist per-action settings across reloads | useSettings<T>() |
| Plugin-wide shared config | useGlobalSettings<T>() |
| Simple cross-action state (built-in) | useChannel() via Action Coordinator |
| Know which actions are visible | useActionPresence() via Action Coordinator |
| Shared state across actions (no provider needed) | Zustand store in module scope |
| Shared state with provider pattern | Jotai/React Context via wrapper |
| Complex derived state / middleware | Zustand or Jotai |