DOCS / AUDIO & NATIVE
Presets
Every shipping plugin rebuilds the same preset boilerplate: factory sounds, user saves, prev/next, a dirty asterisk. VSReacT ships it once — vsreact::PresetManager on the C++ side, usePresets() and <PresetBrowser> on the React side.
The C++ side
Construct a manager next to your ParameterBridge, chain its handleNativeCall, and attach() the RootView. Factory presets are parameter values in natural units — the same numbers you'd dial in by hand. User presets are full APVTS snapshots saved to userApplicationDataDirectory/<appName>/Presets.
vsreact::PresetManager::Options options;
options.appName = "My Plugin";
options.factoryPresets = {
{ "Default", { { "threshold", -18.0f }, { "ratio", 4.0f } } },
{ "Drum Smash",{ { "threshold", -30.0f }, { "ratio", 10.0f } } },
};
presets = std::make_unique<vsreact::PresetManager> (state, options);
rootOptions.onNativeCall = [this] (const juce::String& name, const juce::var& args) -> juce::var {
if (auto handled = presets->handleNativeCall (name, args)) return *handled;
if (auto handled = bridge.handleNativeCall (name, args)) return *handled;
return {};
};
// after constructing the RootView:
presets->attach (*root);Dirty tracking is automatic: the manager listens to every parameter, so the first tweak after a load or save pushes a preset event with dirty: true — the UI's asterisk appears without polling, and self-inflicted changes (the load itself) don't count as edits.
The React side
<PresetBrowser /> is the strip every plugin header has — prev/next arrows, the current name (asterisk when edited) opening a grouped menu (factory first, then user saves), and a save dialog. Drop it anywhere; multiple instances stay in sync because all state lives on the C++ side.
import { PresetBrowser, usePresets } from "@vsreact/core";
<PresetBrowser width={200} />
// or headless, for a custom UI:
const { presets, current, dirty, load, save, next, prev } = usePresets();The wire protocol
| CALL / EVENT | SHAPE |
|---|---|
preset:list | {current, dirty, presets: [{name, factory}]} |
preset:load / save / delete | {name} → {ok} — factory names are reserved against save/delete |
preset:next / prev | walks factory-then-user order and wraps |
event preset | full state after load/save/delete, and when a tweak first dirties the preset |
Resizable editors
Related ship-readiness boilerplate, worth the four lines: because panels are flexbox, a resizable editor reflows instead of scaling. Read the saved size before installing the constrainer — its limits fire resized(), and persisting from that call would clobber the saved size with the construction-time bounds.
const int savedW = (int) state.state.getProperty ("uiWidth", 720);
const int savedH = (int) state.state.getProperty ("uiHeight", 420);
setResizable (true, true);
setResizeLimits (600, 360, 1280, 800);
setSize (savedW, savedH);
constructed = true;
void resized() override
{
root->setBounds (getLocalBounds());
if (constructed) // survives the session via get/setStateInformation
{
state.state.setProperty ("uiWidth", getWidth(), nullptr);
state.state.setProperty ("uiHeight", getHeight(), nullptr);
}
}The compressor example wires all of this — presets, factory sounds, resize with persistence — in vsreact/examples/compressor.