React frontend module development
Mango supports custom frontend modules built with React as an alternative to the traditional AngularJS-based approach. React modules use the Mango REST API for data access and can be packaged as standard Mango modules for distribution and installation. This page covers how to set up a React-based user module, communicate with the Mango backend, build data visualization components, and deploy the finished module.
Overview
Mango's module system allows developers to extend the UI with custom pages, dashboards, and components. While the built-in UI is AngularJS-based, the module packaging system is framework-agnostic: any JavaScript application that can be compiled to static assets can be served as a Mango user module.
React is a good choice for new module development because of its large ecosystem, component-based architecture, and strong tooling. A React module communicates with Mango exclusively through the REST API, which means you can use any HTTP client library and are not tied to Mango's internal AngularJS services.
Prerequisites
Before you begin, make sure you have:
- Mango 5.x installed and running
- Node.js 18+ and npm installed on your development machine
- Basic familiarity with React and modern JavaScript (ES6+)
- A Mango user account with appropriate permissions for the features your module will access
- The module development overview read and understood
Setting up a React user module
Step 1: Create the module project structure
A Mango module follows a specific directory layout. Create a new directory for your module and initialize it:
mkdir mango-react-module
cd mango-react-module
npm init -y
Create the standard module directory structure:
mango-react-module/
├── pom.xml # Maven build descriptor (for Mango module packaging)
├── module.properties # Module metadata
├── web-src/ # React source code
│ ├── src/
│ │ ├── index.jsx # Application entry point
│ │ ├── App.jsx # Root component
│ │ ├── components/ # Reusable components
│ │ ├── hooks/ # Custom React hooks
│ │ ├── services/ # API service layer
│ │ └── styles/ # CSS or CSS modules
│ ├── public/
│ │ └── index.html # HTML shell (for development)
│ ├── package.json
│ └── webpack.config.js # Build configuration
├── web/
│ └── module-name/ # Compiled output (built assets go here)
└── src/
└── com/infiniteautomation/ # Java source (if module has backend components)
Step 2: Configure module.properties
The module.properties file tells Mango about your module:
name=reactModule
version=1.0.0
coreVersion=5.0.0
descriptionKey=reactModule.description
vendor=Your Organization
vendorUrl=https://your-organization.com
Step 3: Set up the React application
Install React and the build toolchain:
cd web-src
npm install react react-dom
npm install --save-dev webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-react css-loader style-loader html-webpack-plugin
Create a minimal webpack configuration (web-src/webpack.config.js):
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (env, argv) => ({
entry: './src/index.jsx',
output: {
path: path.resolve(__dirname, '../web/reactModule'),
filename: 'bundle.[contenthash].js',
publicPath: '/modules/reactModule/',
clean: true,
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
}),
],
devServer: {
port: 3000,
proxy: [{
context: ['/rest'],
target: 'http://localhost:8080',
changeOrigin: true,
}],
},
});
The devServer.proxy setting forwards API requests to a running Mango instance during development, so you can develop the React UI with hot reloading while hitting the real backend.
Using the Mango REST API from React
Authentication
Before making API calls, your module needs to authenticate. If the module runs inside the Mango UI (served as a module page), the user's existing session cookie is used automatically. For standalone development, you need to log in first:
// services/auth.js
const BASE_URL = '/rest/latest';
export async function login(username, password) {
const response = await fetch(`${BASE_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status}`);
}
return response.json();
}
Fetching data point values
The most common operation in a Mango UI module is reading data point values. Create a service layer to encapsulate API calls:
// services/dataPoints.js
const BASE_URL = '/rest/latest';
export async function getPointByXid(xid) {
const response = await fetch(`${BASE_URL}/data-points/by-xid/${xid}`, {
credentials: 'same-origin',
});
if (!response.ok) throw new Error(`Failed to fetch point: ${response.status}`);
return response.json();
}
export async function getPointValue(xid) {
const response = await fetch(`${BASE_URL}/point-values/latest/${xid}?limit=1`, {
credentials: 'same-origin',
});
if (!response.ok) throw new Error(`Failed to fetch value: ${response.status}`);
const values = await response.json();
return values.length > 0 ? values[0] : null;
}
export async function getPointHistory(xid, from, to, rollup, rollupPeriod) {
const params = new URLSearchParams({
from: from.toISOString(),
to: to.toISOString(),
rollup,
timePeriodType: rollupPeriod.type,
timePeriods: rollupPeriod.periods,
});
const response = await fetch(
`${BASE_URL}/point-values/time-period/${xid}?${params}`,
{ credentials: 'same-origin' },
);
if (!response.ok) throw new Error(`Failed to fetch history: ${response.status}`);
return response.json();
}
Real-time updates with WebSockets
Mango provides WebSocket endpoints for receiving real-time point value updates without polling:
// hooks/usePointValue.js
import { useState, useEffect } from 'react';
export function usePointValue(xid) {
const [value, setValue] = useState(null);
useEffect(() => {
if (!xid) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/rest/latest/websocket/point-value`);
ws.onopen = () => {
ws.send(JSON.stringify({
requestType: 'REGISTER',
xid,
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.xid === xid && data.value !== undefined) {
setValue(data);
}
};
return () => {
ws.close();
};
}, [xid]);
return value;
}
Use this hook in any component:
function TemperatureDisplay({ xid }) {
const pointValue = usePointValue(xid);
if (!pointValue) return <div>Loading...</div>;
return (
<div className="temperature-display">
<span className="value">{pointValue.value.toFixed(1)}</span>
<span className="unit">°F</span>
<span className="timestamp">
{new Date(pointValue.timestamp).toLocaleTimeString()}
</span>
</div>
);
}
Component patterns for data visualization
Reusable data point wrapper
Create a higher-level component that handles loading, error states, and data fetching:
// components/DataPointValue.jsx
import { useState, useEffect } from 'react';
import { getPointByXid, getPointValue } from '../services/dataPoints';
export function DataPointValue({ xid, render }) {
const [point, setPoint] = useState(null);
const [value, setValue] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
const [pointData, valueData] = await Promise.all([
getPointByXid(xid),
getPointValue(xid),
]);
if (!cancelled) {
setPoint(pointData);
setValue(valueData);
}
} catch (err) {
if (!cancelled) setError(err.message);
}
}
fetchData();
return () => { cancelled = true; };
}, [xid]);
if (error) return <div className="error">Error: {error}</div>;
if (!point || !value) return <div className="loading">Loading...</div>;
return render({ point, value });
}
Table-based dashboard component
A common pattern is a table displaying multiple point values:
// components/PointTable.jsx
import { usePointValue } from '../hooks/usePointValue';
function PointRow({ xid, label }) {
const pointValue = usePointValue(xid);
return (
<tr>
<td>{label}</td>
<td>{pointValue ? pointValue.value : '--'}</td>
<td>{pointValue ? new Date(pointValue.timestamp).toLocaleString() : '--'}</td>
</tr>
);
}
export function PointTable({ points }) {
return (
<table>
<thead>
<tr>
<th>Point</th>
<th>Value</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{points.map(({ xid, label }) => (
<PointRow key={xid} xid={xid} label={label} />
))}
</tbody>
</table>
);
}
Integrating charting libraries
For charts in a React module, use a React-compatible charting library such as Recharts, Chart.js (with react-chartjs-2), or Apache ECharts (with echarts-for-react). Here is an example using Recharts:
// components/HistoryChart.jsx
import { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import { getPointHistory } from '../services/dataPoints';
export function HistoryChart({ xid, hours = 24 }) {
const [data, setData] = useState([]);
useEffect(() => {
const to = new Date();
const from = new Date(to.getTime() - hours * 60 * 60 * 1000);
getPointHistory(xid, from, to, 'AVERAGE', { type: 'MINUTES', periods: 15 })
.then((values) => {
setData(values.map((v) => ({
time: new Date(v.timestamp).toLocaleTimeString(),
value: v.value,
})));
});
}, [xid, hours]);
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="value" stroke="#1976d2" dot={false} />
</LineChart>
</ResponsiveContainer>
);
}
Building and deploying React modules
Development workflow
During development, use the webpack dev server with API proxying:
cd web-src
npm run dev
This starts the React application on port 3000 and proxies all /rest requests to the Mango instance running on port 8080. You get hot module reloading for rapid development.
Production build
To build the module for deployment:
cd web-src
npx webpack --mode production
This compiles and minifies the React application into the web/reactModule/ directory, where Mango serves it as a module page.
Module packaging with Maven
If your module includes Java backend components (REST endpoints, data source definitions), the Maven build handles both the Java compilation and the frontend asset packaging:
mvn install
The resulting .zip file in the target/ directory is a complete Mango module that can be installed through the Modules page in the Mango UI or placed directly in the mango-data/modules/ directory.
Module registration for custom pages
To register your React module's pages in the Mango navigation menu, add a menu item configuration. This is typically done in the module's Java definition class or through a module page declaration that maps a URL path to the module's index.html.
For a module that only contains frontend pages (no Java backend), you can use the web/ directory structure alone and register the page through a module.json configuration file:
{
"pages": [
{
"url": "/ui/react-module",
"templateUrl": "modules/reactModule/index.html",
"menuText": "React Module",
"menuIcon": "dashboard"
}
]
}
Key differences from AngularJS modules
If you are familiar with the existing AngularJS module development approach, here are the key differences when building with React:
| Aspect | AngularJS module | React module |
|---|---|---|
| Data binding | Two-way binding via ng-model | One-way data flow with state and props |
| API access | Use maPoint, maDataSource AngularJS services | Use fetch() or Axios with the REST API directly |
| Real-time updates | Built-in via maPointValues service | Implement WebSocket connection manually (see hook above) |
| UI framework | AngularJS Material components | Any React component library (MUI, Ant Design, etc.) |
| Build system | Mango's built-in webpack pipeline | Your own webpack/Vite configuration |
| Routing | ui-router states registered via module API | React Router or any client-side router |
| Form validation | ng-form with ng-messages | React Hook Form, Formik, or custom validation |
| Component registration | AngularJS module.component() | Standard React component exports |
Best practices
- Use the REST API versioned path (
/rest/latestor/rest/v2) to ensure compatibility across Mango updates. - Handle authentication errors gracefully -- If a 401 response is received, redirect the user to the login page or display an appropriate message.
- Debounce rapid API calls -- When building search interfaces or autocomplete fields that query the API, use debouncing to avoid overwhelming the server.
- Cache static configuration -- Data point configurations change rarely. Fetch them once and cache them in component state or a React context rather than re-fetching on every render.
- Follow Mango's permission model -- Check the user's permissions before displaying UI elements that require specific access. Use the
/rest/latest/users/currentendpoint to retrieve the current user's roles. - Keep bundle size small -- Use code splitting and lazy loading for large modules to reduce initial load time.
Related pages
- Module development overview -- General concepts for Mango module development
- AngularJS frontend module development -- The traditional AngularJS approach for comparison
- Module setup -- Setting up the module development environment
- Module configuration -- Configuring module metadata and dependencies