React Plugin Development¶
FuncNodes modules can include React plugins to provide custom UI components — specialized input editors, output previews, and widgets that enhance the user experience for your node types.
Overview¶
React plugins integrate with @linkdlab/funcnodes_react_flow (the FuncNodes UI host) and can provide:
- Custom Previews — Rich rendering of output values (charts, images, 3D views)
- Custom Inputs — Specialized editors (color pickers, molecule editors, file browsers)
- Custom Widgets — Additional UI for node headers or panels
my_module/
├── src/funcnodes_mymodule/
│ ├── __init__.py
│ ├── nodes.py
│ └── _react_plugin.py # Plugin info export
└── react_plugin/
├── package.json
├── vite.config.ts
├── tsconfig.json
└── src/
└── index.tsx # Plugin implementation
Quick Start¶
1. Scaffold with funcnodes_module¶
This creates a complete React plugin scaffold ready to customize.
2. Manual Setup¶
Create the plugin directory:
Create package.json:
{
"name": "funcnodes-mymodule-react",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite build --watch",
"build": "vite build"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@linkdlab/funcnodes_react_flow": "latest",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^5.0.0"
},
"peerDependencies": {
"@linkdlab/funcnodes_react_flow": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Create vite.config.ts:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: resolve(__dirname, "src/index.tsx"),
name: "FuncNodesPlugin", // MUST be "FuncNodesPlugin"
formats: ["es"],
fileName: () => "funcnodes_mymodule_react.es.js"
},
rollupOptions: {
external: [
"react",
"react-dom",
"@linkdlab/funcnodes_react_flow"
],
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
"@linkdlab/funcnodes_react_flow": "FuncNodesReactFlow"
}
}
},
outDir: "../src/funcnodes_mymodule/react_plugin"
}
});
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Plugin Implementation¶
Basic Plugin Structure¶
Create src/index.tsx:
import React from "react";
import {
FuncNodesReactPlugin,
RenderNodeOutputProps,
RenderNodeInputProps,
} from "@linkdlab/funcnodes_react_flow";
// Custom preview component
const MyTypePreview: React.FC<{ value: any }> = ({ value }) => {
return (
<div style={{ padding: 8, background: "#f0f0f0", borderRadius: 4 }}>
<pre>{JSON.stringify(value, null, 2)}</pre>
</div>
);
};
// Custom input component
const MyTypeInput: React.FC<{
value: any;
onChange: (value: any) => void;
}> = ({ value, onChange }) => {
return (
<input
type="text"
value={value || ""}
onChange={(e) => onChange(e.target.value)}
style={{ width: "100%" }}
/>
);
};
// Plugin definition
const MyPlugin: FuncNodesReactPlugin = {
// Render custom output previews
renderNodeOutput: (props: RenderNodeOutputProps) => {
const { type, value, fullscreen } = props;
if (type === "MyCustomType") {
return <MyTypePreview value={value} />;
}
// Return null to use default renderer
return null;
},
// Render custom input editors
renderNodeInput: (props: RenderNodeInputProps) => {
const { type, value, onChange, render_options } = props;
if (render_options?.type === "my_custom_input") {
return <MyTypeInput value={value} onChange={onChange} />;
}
return null;
},
};
// MUST export as default
export default MyPlugin;
Register the Plugin in Python¶
Create src/funcnodes_mymodule/_react_plugin.py:
from pathlib import Path
# Path to the built React plugin
REACT_PLUGIN = {
"js": Path(__file__).parent / "react_plugin" / "funcnodes_mymodule_react.es.js",
}
Update __init__.py:
Update pyproject.toml entry points:
[project.entry-points."funcnodes.module"]
module = "funcnodes_mymodule"
shelf = "funcnodes_mymodule:NODE_SHELF"
react_plugin = "funcnodes_mymodule:REACT_PLUGIN"
Plugin API Reference¶
FuncNodesReactPlugin Interface¶
interface FuncNodesReactPlugin {
// Render custom output previews
renderNodeOutput?: (props: RenderNodeOutputProps) => React.ReactNode | null;
// Render custom input editors
renderNodeInput?: (props: RenderNodeInputProps) => React.ReactNode | null;
// Render custom node header content
renderNodeHeader?: (props: RenderNodeHeaderProps) => React.ReactNode | null;
// Called when plugin is loaded
onLoad?: () => void;
// Called when plugin is unloaded
onUnload?: () => void;
}
RenderNodeOutputProps¶
interface RenderNodeOutputProps {
// The type string of the output
type: string;
// Current output value
value: any;
// Whether rendering in fullscreen/expanded mode
fullscreen: boolean;
// Node UUID
nodeId: string;
// Output ID
outputId: string;
// Render options from the node definition
render_options?: Record<string, any>;
}
RenderNodeInputProps¶
interface RenderNodeInputProps {
// The type string of the input
type: string;
// Current input value
value: any;
// Callback to update the value
onChange: (value: any) => void;
// Node UUID
nodeId: string;
// Input ID
inputId: string;
// Value constraints (min, max, options, etc.)
value_options?: {
min?: number;
max?: number;
step?: number;
options?: string[] | { type: "enum"; keys: string[]; values: any[] };
};
// Render options from the node definition
render_options?: Record<string, any>;
// Whether input is disabled
disabled?: boolean;
}
Common Plugin Patterns¶
Image Preview¶
const ImagePreview: React.FC<{ value: any }> = ({ value }) => {
if (!value) return null;
// Assume value is base64 encoded image
const src = `data:image/png;base64,${value}`;
return (
<img
src={src}
alt="Preview"
style={{ maxWidth: "100%", maxHeight: 300 }}
/>
);
};
const plugin: FuncNodesReactPlugin = {
renderNodeOutput: (props) => {
if (props.type === "ImageFormat" || props.type === "np.ndarray") {
return <ImagePreview value={props.value} />;
}
return null;
}
};
Color Picker Input¶
const ColorInput: React.FC<{
value: string;
onChange: (value: string) => void;
}> = ({ value, onChange }) => {
return (
<input
type="color"
value={value || "#000000"}
onChange={(e) => onChange(e.target.value)}
style={{ width: 40, height: 24, padding: 0, border: "none" }}
/>
);
};
const plugin: FuncNodesReactPlugin = {
renderNodeInput: (props) => {
if (props.render_options?.type === "color") {
return <ColorInput value={props.value} onChange={props.onChange} />;
}
return null;
}
};
Plotly Chart Preview¶
import Plot from "react-plotly.js";
const PlotlyPreview: React.FC<{ value: any; fullscreen: boolean }> = ({
value,
fullscreen
}) => {
if (!value) return null;
const { data, layout } = value;
return (
<Plot
data={data}
layout={{
...layout,
width: fullscreen ? 800 : 300,
height: fullscreen ? 600 : 200,
margin: { t: 20, r: 20, b: 30, l: 40 }
}}
config={{ displayModeBar: fullscreen }}
/>
);
};
Dropdown with Custom Styling¶
const StyledDropdown: React.FC<{
value: string;
options: string[];
onChange: (value: string) => void;
}> = ({ value, options, onChange }) => {
return (
<select
value={value || ""}
onChange={(e) => onChange(e.target.value)}
style={{
width: "100%",
padding: "4px 8px",
borderRadius: 4,
border: "1px solid #ccc"
}}
>
{options.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
);
};
File Upload Input¶
const FileUploadInput: React.FC<{
onChange: (file: File) => void;
}> = ({ onChange }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
onChange(file);
}
};
return (
<input
type="file"
onChange={handleChange}
style={{ width: "100%" }}
/>
);
};
Building and Deploying¶
Development Build¶
Production Build¶
The built file lands in src/funcnodes_mymodule/react_plugin/.
Include in Package¶
Ensure the built JS file is included in your Python package:
# pyproject.toml
[tool.hatch.build.targets.wheel]
packages = ["src/funcnodes_mymodule"]
# Include the react_plugin directory
[tool.hatch.build.targets.wheel.force-include]
"src/funcnodes_mymodule/react_plugin" = "funcnodes_mymodule/react_plugin"
Or with setuptools:
# setup.py or pyproject.toml
package_data = {
"funcnodes_mymodule": ["react_plugin/*.js", "react_plugin/*.css"]
}
Styling¶
CSS Modules¶
// styles.module.css
.preview {
padding: 8px;
border-radius: 4px;
background: var(--fn-bg-secondary);
}
// index.tsx
import styles from "./styles.module.css";
const Preview = () => <div className={styles.preview}>...</div>;
CSS-in-JS¶
const Preview = () => (
<div
style={{
padding: 8,
borderRadius: 4,
background: "var(--fn-bg-secondary)"
}}
>
...
</div>
);
FuncNodes CSS Variables¶
The host provides CSS variables for consistent theming:
| Variable | Description |
|---|---|
--fn-bg-primary |
Primary background |
--fn-bg-secondary |
Secondary background |
--fn-text-primary |
Primary text color |
--fn-text-secondary |
Secondary text color |
--fn-accent |
Accent color |
--fn-border |
Border color |
--fn-radius |
Border radius |
Debugging¶
Development Tips¶
- Use React DevTools — Install the browser extension
- Console logging —
console.log()in your plugin - Hot reload — Use
npm run devfor watch mode - Check Network tab — Verify plugin JS is loaded
Common Issues¶
Plugin not loading:
- Check entry point path in pyproject.toml
- Verify built JS file exists
- Check browser console for errors
Component not rendering:
- Verify type string matches exactly
- Check renderNodeOutput returns JSX, not null
- Ensure default export is the plugin object
Styling issues: - Use CSS variables for theme consistency - Avoid global styles that might conflict
Examples from Official Modules¶
funcnodes_plotly¶
Renders Plotly figures with zoom/pan controls:
// Simplified example
const PlotlyPlugin: FuncNodesReactPlugin = {
renderNodeOutput: (props) => {
if (props.type === "plotly.graph_objs.Figure") {
return <PlotlyRenderer figure={props.value} fullscreen={props.fullscreen} />;
}
return null;
}
};
funcnodes_files¶
File browser and upload widgets:
const FilesPlugin: FuncNodesReactPlugin = {
renderNodeInput: (props) => {
if (props.render_options?.type === "file_browser") {
return <FileBrowser onSelect={props.onChange} />;
}
return null;
}
};
funcnodes_rdkit¶
Molecule structure editor using JSME:
const RDKitPlugin: FuncNodesReactPlugin = {
renderNodeInput: (props) => {
if (props.type === "Mol" && props.render_options?.editor) {
return <MoleculeEditor smiles={props.value} onChange={props.onChange} />;
}
return null;
},
renderNodeOutput: (props) => {
if (props.type === "Mol") {
return <MoleculePreview smiles={props.value} />;
}
return null;
}
};
See Also¶
- Writing Modules — Complete module guide
- Testing Modules — Testing your module
- Inputs & Outputs — Render options reference
- funcnodes_plotly — Example with React plugin