Soundorv0.13.0

React UI

How a plugin UI is written with React: components, styles, events, state and parameters.

Source: packages/react/.

A plugin's UI is a React app. @soundor/react renders it into the plugin view (soundor:ui), with the components and conventions of React Native:

import { Pressable, Text, View, useParameter } from '@soundor/react';
import { parameters } from 'soundor:parameters';

// A parameter's value, re-rendered whenever it changes: here, from the
// buttons; in a plugin, from host automation and presets too.
export default function Gain() {
  const gain = useParameter(parameters.gain);
  const step = (by: number) => parameters.gain.set(gain + by);

  return (
    <View style={{ alignItems: 'center', gap: 12 }}>
      <Text style={{ fontSize: 28, color: '#e6e7ea' }}>
        Gain {gain.toFixed(2)}
      </Text>
      <View style={{ flexDirection: 'row', gap: 8 }}>
        {[
          { label: '−', onPress: () => step(-0.1) },
          { label: 'Reset', onPress: () => parameters.gain.set(0.5) },
          { label: '+', onPress: () => step(0.1) },
        ].map(({ label, onPress }) => (
          <Pressable
            key={label}
            onPress={onPress}
            style={({ pressed, hovered }) => ({
              minWidth: 44,
              alignItems: 'center',
              paddingVertical: 8,
              paddingHorizontal: 14,
              borderRadius: 8,
              backgroundColor: pressed
                ? '#2a5fd0'
                : hovered
                  ? '#4f8bff'
                  : '#3a7bfd',
            })}
          >
            <Text style={{ color: 'white', fontSize: 15 }}>{label}</Text>
          </Pressable>
        ))}
      </View>
    </View>
  );
}
gain.tsxSource

The snippets on this site run live, in the web runtime: edit one, then apply. A plugin's src/main.tsx renders its root component with render(<App />).

No DOM

In a native plugin the UI runs inside Soundor's embedded JavaScript runtime, without a browser: there is no DOM, no window, no CSS. The web runtime draws the same Soundor nodes with DOM elements. Either way the UI sees the same components, the same layout and the same events, so it is written once:

  • Components are React Native's: View, Text, Pressable, TextInput, ScrollView, Image, plus Canvas for code that draws, and overlays: Portal, Modal, FocusScope. Strings go in a Text.
  • Styles are objects, not CSS: flexbox, a column by default (Style).
  • Events are Web-style pointer, wheel, key and focus events that capture and bubble (Events), in the view's logical pixels (coordinates).
  • Accessibility is React Native's props, which every runtime presents to its platform's screen readers (Accessibility).
  • Animation runs in the runtime, not in React: Animated.View animates style changes, and useAnimationFrame draws a canvas every frame.

State and parameters

What the host automates and saves is a parameter, not React state: read one with useParameter, and change it with set(), so the host, its automation and presets, and every view show the same value.

const gain = useParameter(parameters.gain);
// …
parameters.gain.beginGesture();
parameters.gain.set(next); // while dragging
parameters.gain.endGesture();

Keep React state for what only the UI knows: a tab, a menu that is open. Persistent preferences go in soundor:storage, files in soundor:fs. See Parameters and @soundor/react for rendering and the hooks.

Testing

Install vitest, @soundor/vitest, vitest-environment-soundor, and @soundor/testing-library as direct development dependencies. Configure plugins: [soundor()] from @soundor/vitest and test.environment: 'soundor'. Tests run against a simulated Soundor runtime, using the real React reconciler.

import { Pressable, Text } from '@soundor/react';
import { render, screen, userEvent } from '@soundor/testing-library/react';
import { expect, test, vi } from 'vitest';

test('activates a control', async () => {
  const onPress = vi.fn();
  const user = userEvent.setup();
  await render(
    <Pressable onPress={onPress}>
      <Text>Save</Text>
    </Pressable>,
  );
  await user.click(screen.getByRole('button', { name: 'Save' }));
  expect(onPress).toHaveBeenCalledOnce();
});

See the Testing guide for installation, configuration, queries, mocks, async updates, animation time, and limits. In this repository, pnpm --filter @soundor/react test runs the private testing workspace, which owns its direct testing dependencies and keeps the renderer's build independent of its test tools.

On this page