I’ve spent the last months building a React renderer for JUCE editors and
it’s now at the point where I’d genuinely rather write panels this way, so
I’m sharing it.
What it is. Your UI is TSX running in an embedded QuickJS engine, on the
message thread. A custom react-reconciler streams mutations over a C bridge;
the module keeps a C++ shadow tree, lays it out with Yoga (real flexbox),
and paints with juce::Graphics. No webview, no browser process, no DOM. The
editor side is one component:
vsreact::RootOptions options;
options.bundleSource = juce::String::fromUTF8 (BinaryData::main_js,
BinaryData::main_jsSize);
root = std::make_unique<vsreact::RootView> (std::move (options));
bridge.attach (*root); // vsreact::ParameterBridge, wraps your APVTS
addAndMakeVisible (*root);
The parts JUCE people will care about:
- APVTS is the source of truth.
useParameter(id)in TS binds two-way
through the ParameterBridge with the begin/set/end gesture bracket, so
host automation records correctly. Ranges, skew, steps and value text all
come from your NormalisableRange over the bridge — nothing is restated in
JS, so a C++ range change propagates without touching the UI. - Audio thread never sees JS. The engine lives on the message thread;
meter/analyzer data crosses via atomics you publish and a
sendNativeEventyou call from a Timer. - Hot reload in the DAW. Point RootOptions at the bundle file in dev
builds and it re-evaluates on save (~100ms). FL Studio never closes. Ship
builds embed the bundle via BinaryData. - Native escape hatch.
<NativeView nativeId="scope">mounts any
juce::Component inside the React layout — existing custom components keep
working. - Ship-readiness included.
vsreact::PresetManager+usePresets()+
a stock<PresetBrowser>strip: factory presets are parameter values in
natural units, user presets are APVTS snapshots in per-user app data,
dirty tracking ignores self-inflicted loads. Editors can be resizable
with the size persisted through your APVTS tree. And when TSX throws in
the DAW, the error overlay namessrc/main.tsxlines — the runtime
translates stacks through the bundle’s own source map. - Performance. QuickJS has no JIT and doesn’t need one here: JS runs on
renders, C++ runs every frame. Layout, painting, hover and hit-testing
never enter the engine. A painter stress scene (96 glowing segments, 8
multi-layer gradient caps) paints in ~8ms, and that number is a CI gate,
not a slide.
Proof it holds up: StashTrack (a commercial VST) runs its whole UI on
it; the repo carries six example plugins built from the stock component set,
each themed to its own hardware character (and the pixel-exact designer-art
workflow StashTrack uses — plate + invisible hit zones + film-strip knobs —
has a docs guide of its own); all six pass pluginval at strictness 5, and CI
runs the C++ suite on Windows, macOS and Linux plus pluginval (VST3, both
the simplest and the busiest example) and auval (AU) on every commit.
Current limits, honestly: AAX/LV2 are untested (VST3, AU and Standalone
are machine-validated on every commit — pluginval at strictness 5 on Windows,
auval on macOS), and native-side hover/active style merges don’t animate.
Linux builds
and passes the full suite in CI — adding that job caught something worth
knowing about even if you never touch this project: JUCE 8.0.4’s Linux glyph
fallback segfaults in SimpleShapedText::shape when a custom font is missing
a glyph for a non-ASCII character. Fixed by 8.0.14, so that’s the Linux
floor here.
MIT licensed (the renderer — JUCE’s license still applies to your plugin as
usual). Quick start is npm create vsreact my-plugin, or read the gain
example: a themed two-knob panel with metering, ~130 lines of TSX and no
C++ beyond the processor.
Site + docs: https://vsreact.n9records.com
Repo: GitHub - N9RecordsTechnologiesIL/VSReacT: Write React. Ship native VST. A React renderer for JUCE audio plugins — QuickJS + Yoga + juce::Graphics, no webview. · GitHub
Would love to hear where it breaks for your use cases — especially from
anyone with strong opinions about GUI frameworks on this forum, which I
believe is everyone.