Limitations of JSON UI components
There are two ways define UI components—one uses declarative JSON, and the other uses programmatic tools like React. Developers can use these approaches separately or combine them within the same application.
Benefits of JSON declarative components
JSON components define user interfaces using a declarative configuration. Developers specify what data to display and how to present it in JSON, rather than writing control flow or rendering logic. When you run the UI bundler, it analyzes all declarative JSON configurations in your application package. It then generates standard React and Redux code to build a complete Node.js, React, and Redux application. The bundler compiles this application into static assets — HTML, CSS, and JavaScript — for deployment.
Declarative components support several built-in capabilities:
- Fetch backend data automatically using a
dataSpec - Handle error and loading states without additional logic
- Accept user input through predefined UI elements
- Apply theming, localization, and accessibility automatically
The following example configures a basic data grid that displays records from the WindTurbine entity:
// {package}/ui/c3/meta/examplePackage.TurbineGrid.json
{
"type": "UiSdlConnected<UiSdlDataGrid>",
"component": {
"dataSpec": {
"dataType": "WindTurbine",
"columnFields": [
{
"fieldName": "location",
"label": "Turbine location"
},
{
"fieldName": "status",
"label": "Status"
}
]
}
}
}This configuration renders a data grid with two columns: location and status. The platform handles the data request automatically based on the dataType. There is a rich set of configurations available for each UI Component that application developers can enable by checking the API reference documentation for the particular UI component.
JSON and programmatic constructs
JSON works well for declaring intent, but sometimes your intent depends on context. For example, you might want to render one component instead of another based on a condition. Because JSON is static and declarative, it cannot represent dynamic constructs or runtime logic. As a result, it does not support:
- Conditional rendering based on arbitrary expressions
- Iterating over custom collections or computed data
- Reusing fragments with parameters or injecting child components
- Managing local or shared component state
- Changing layout or behavior in response to user interaction
The platform includes UI Components such as UiSdlConditionalRenderer and UiSdlSwitchRenderer to support basic control flow. These UI Components enable simple conditionals but were not designed to replicate the expressive power of code. The goal of JSON UI components is to create simple declarative configurations for situations where you don't need a lot of conditionals.
The following example renders a banner when the current turbine's status is "FAULT":
{
"type": "UiSdlConditionalRenderer",
"condition": {
"type": "UiDynamicValueRenderCondition",
"param": {
"type": "UiSdlApplicationStateValueParam",
"path": "turbineIsFaulty"
}
},
"useElseComponentAsFallback": true,
"thenComponent": {
"id": "examplePackage.FaultyNotificationBanner"
},
"elseComponent": {
"id": "examplePackage.NormalNotificationBanner"
}
}This pattern handles a single boolean condition. However, it does not scale well for complex decision trees, nested conditions, or reusable component patterns. The logic must be embedded directly in JSON and cannot branch, loop, or compose components dynamically.
Composing UI JSON components
To make the JSON configuration experience simple, the platform designs each UI component to include extensive built-in functionality. Each component behaves like a black box and exposes a configuration interface through JSON.
Most JSON UI components were not designed for nesting or composition. Instead of letting you insert arbitrary children, each component defines a fixed structure. If a component does not expose a specific feature through its JSON API, you cannot add that functionality by injecting another component inside it.
This creates a tradeoff between simplicity and flexibility. For example, if you want to insert a custom button inside a filter panel:
You can configure it with JSON only if the filter panel component explicitly supports that option.
If it doesn’t, and you need fine-grained layout or interactive behavior, you should use React to implement the feature.
Use the table below to compare when to use JSON versus React:
| Goal | Use JSON UI Component if... | Use React if... |
|---|---|---|
| Basic layout and display logic | The component supports your use case out of the box | You need custom structure or layout not exposed in JSON |
| Reuse with custom children | You can configure everything through JSON | You want to insert reusable child components |
| Injecting interactivity (buttons, actions) | The component supports the behavior directly in JSON | You need to add interactive elements not supported by the component |
| Fine-grained control over layout or behavior | The layout logic matches the component’s defaults | You need exact control over structure or interactivity |
Why React supports composition
React gives you full control over rendering logic, structure, and behavior. You can define components programmatically, apply conditions, iterate over data, and compose reusable elements. This model allows you to express complex UI patterns that JSON cannot represent.
React components support:
- All JavaScript expressions, control structures, and functions
- Modular composition using props and children
- Local and global state management
- Integration with Redux, application state, and external libraries
- You have full control over what the application renders
The following example renders a list of turbines using a reusable child component:
// {package}/ui/c3/src/customInstances/TurbineGrid.tsx
import React from 'react';
import { useC3Action } from '@c3/ui/UiSdlUseC3Action';
// Define a child component for each row
const TurbineRow = ({ turbine }) => {
// Apply conditional formatting if the turbine is faulted
const isFaulted = turbine.status === 'FAULT';
return (
<div style={{ color: isFaulted ? 'red' : 'black' }}>
{turbine.name} — {turbine.status}
</div>
);
};
const TurbineGrid = () => {
// Fetch turbine data from the backend
const { status, data } = useC3Action({
typeName: 'WindTurbine',
actionName: 'fetch',
method: 'POST'
});
// Show a loading message while the data is being fetched
if (status !== 'done') return <div>Loading turbines...</div>;
// Render a list of turbine rows
return (
<div>
{data.map(turbine => (
<TurbineRow key={turbine.id} turbine={turbine} />
))}
</div>
);
};
export default TurbineGrid;This example demonstrates key features of React composition:
useC3Actionhandles backend communication and status tracking- The component uses
mapto iterate through records - Each row is rendered using a reusable, parameterized child component (
TurbineRow) - Conditional styling is applied inline based on the turbine status
Declarative JSON cannot replicate this pattern. It lacks native support for child components, dynamic conditions, and logic-driven rendering.
Comparison: JSON vs. React capabilities
| Feature | JSON Components | React Components |
|---|---|---|
| Data fetching | Built-in via dataSpec | Custom with useC3Action, epics |
| Error/loading handling | Built-in | Fully customizable |
| Conditional rendering | Limited via render conditions | Native via if, ternaries, JSX |
| Loops and iteration | Iterator components like Grid and Card list | Native via map, for, etc. |
| Reusable UI fragments | Iterator components like Grid and Card list | Native via props and child components |
| Theming and translations | Automatic | Manual or opt-in |
| Dynamic layout or behavior | Iterator components like Grid and Card list | Supported through code |
| Custom interactivity (state) | Iterator components like Grid and Card list | Native via useState, Redux |
| Third-party integration (such as MUI) | Not supported | Supported through code |
Switching from JSON to React
In many applications, developers begin with a JSON layout and later discover the need for more control. Consider this JSON component that renders a simple button:
{
"type" : "UiSdlConnected<UiSdlButton>",
"component" : {
"content" : "Submit"
}
}This setup works for trivial use cases. However, once the button must:
- Disable based on multiple field values
- Show a dynamic label
- Submit a request with runtime arguments
- Render as part of a form group
then JSON becomes harder to maintain. Switching to React enables all of these capabilities in a single file.
// SubmitButton.tsx
import React from 'react';
const SubmitButton = ({ disabled, label, onSubmit }) => {
return (
<button disabled={disabled} onClick={onSubmit}>
{label}
</button>
);
};This version accepts props, handles logic, and can be reused across many pages.
Using JSON and React together
The platform supports mixed component strategies. You can combine JSON and React on the same page, within the same module, or across different parts of the application.
You can:
- Reference a React component from JSON using a
UiSdlComponentRef - Import and render JSON-based components from within a React page
- Share Redux state and application state across both component types
For example, the following JSON configuration renders a custom React component:
{
"type": "UiSdlLayoutNavMenu",
"navMenu": {
"id": "App.Nav"
},
"children": [
{
"id": "App.CustomComponent"
}
]
}In this pattern, App.CustomComponent is implemented as a .tsx file in the customInstances folder. It does not receive props from JSON. Instead, it must access context, application state, or Redux to manage its behavior.
This hybrid approach allows teams to start with JSON for basic views and incrementally adopt React for custom logic or reusable features. It supports flexibility without requiring a full rewrite.