Create a fully reusable React component
Create a reusable React component to define a UI component once and reuse it across pages, modules, or applications. Write the logic and layout in a .tsx file, and expose configuration through JSON metadata.
Choose this pattern when the component must:
- Render in multiple places
- Accept props
- Support SDL-based configuration, introspection, and overrides
- If SDL does not support your use case, custom components are a good option
Install libraries
Declare third-party libraries in the platform runtime. The following snippet defines and resolves Material UI libraries in the js-webpack_c3 runtime.
// Create a runtime and define its libraries
let runtime = ImplLanguage.Runtime.fromJson({
name: "js-webpack_c3",
libraries: [
"npm @mui/material@7.1.0", // Material UI core
"npm @emotion/styled@11.14.0" // Emotion styling (peer dependency)
]
});
// Enable developer mode to allow runtime changes
Pkg.setDevMode(true);
// Resolve the runtime and save the lock file
Js.upsertRuntime(runtime, "client");Build a reusable component with no props
This example creates a radar chart using ECharts. It does not receive props or declare a .c3typ Type.
// {package}/ui/c3/src/mui/CustomComponentInstance.tsx
import React, { useEffect, useRef } from 'react'; // Import React and its hooks
import * as echarts from 'echarts'; // Import the ECharts library
const CustomComponentInstance = () => {
const myChart = useRef(null); // Create a ref to mount the chart
useEffect(() => {
const chart = echarts.init(myChart.current); // Initialize chart on mount
const options = { // Define chart configuration
radar: {
indicator: [ // Set radar axis labels
{ name: 'Sales', max: 6500 },
{ name: 'Administration', max: 16000 },
{ name: 'Information Technology', max: 30000 },
{ name: 'Customer Support', max: 38000 },
{ name: 'Engineering', max: 52000 },
{ name: 'Marketing', max: 25000 },
],
},
series: [
{
name: 'Budget', // Label for the dataset
type: 'radar', // Set series type
data: [ // Provide chart data
{
value: [4200, 3000, 20000, 35000, 52000, 18000],
name: 'Allocated Budget',
},
],
},
],
};
chart.setOption(options); // Apply options to chart
window.addEventListener('resize', () => chart.resize()); // Resize on window change
setTimeout(() => chart.resize(), 100); // Delay resize for layout stability
}, []);
return (
<div
ref={myChart} // Attach chart to DOM
style={{ width: '100%', height: '100%' }} // Set container dimensions
></div>
);
};
export default CustomComponentInstance;This component handles its own rendering logic and state internally. It does not rely on props or external configuration. It loads data and renders without relying on props.
Define a reusable component with props (without SDL)
The next example defines a React component that accepts props and displays a list of chips. It does not integrate with SDL, but can be reused throughout your application.
Shared chip component with optional behavior
// {package}/ui/c3/src/mui/ChipsArray.tsx
import * as React from 'react'; // Import React
import Chip from '@mui/material/Chip'; // Import Material UI chip
import Button from '@mui/material/Button'; // Import Material UI button
// Define the structure of a single chip
interface ChipData {
key: number;
label: string;
}
// Define props accepted by the component
interface ChipsArrayProps {
orientation?: 'vertical' | 'horizontal'; // Direction of layout
deleteable?: boolean; // Enable or disable chip deletion
}
// Render a dynamic list of chips
const ChipsArray = (props: ChipsArrayProps) => {
const defaultChipData = [ // Define default chip labels
{ key: 0, label: 'Angular' },
{ key: 1, label: 'jQuery' },
{ key: 2, label: 'Polymer' },
{ key: 3, label: 'React' },
{ key: 4, label: 'Vue.js' },
];
const [chipData, setChipData] = React.useState<readonly ChipData[]>([...defaultChipData]);
const handleDelete = (chipToDelete: ChipData) => () => {
setChipData((chips) => chips.filter((chip) => chip.key !== chipToDelete.key)); // Remove deleted chip
};
const resetChipData = () => {
setChipData([...defaultChipData]); // Restore default chips
};
const getDeleteProps = (data: ChipData) => (props.deleteable ? { onDelete: handleDelete(data) } : {});
return (
<div>
<div style={{ padding: '16px' }}>
{props.deleteable && ( // Conditionally show reset button
<div style={{ marginBottom: '8px' }}>
<Button variant="contained" onClick={resetChipData} color="primary" disableElevation>
Reset Chips
</Button>
</div>
)}
<div style={{ display: 'flex', flexDirection: props.orientation === 'horizontal' ? 'row' : 'column' }}>
{chipData.map((data) => (
<div style={{ display: 'flex' }} key={data.key}>
<Chip label={data.label} color="success" {...getDeleteProps(data)} />
</div>
))}
</div>
</div>
</div>
);
};
export default ChipsArray;Wrap the component for JSON use
To render a React component from SDL metadata, wrap it in a file under the customInstances directory. This wrapper connects the component to the platform’s runtime and allows it to appear in declarative UI configurations.
The following example creates a custom component instance that passes static props to the reusable ChipsArray component:
// {package}/ui/c3/src/customInstances/WindTurbine.CustomComponentChip.tsx
import * as React from 'react';
import ChipsArray from '@c3/app/ui/src/mui/ChipsArray';
// Wrap the reusable ChipsArray with static props
const CustomComponentChip = () => {
return <ChipsArray orientation="horizontal" deleteable={true} />;
};
export default CustomComponentChip;Expose a reusable component through SDL
This final example defines a component that receives props from SDL, uses UiSdlDynamicValueSpec, and supports translation.
Reference the component using JSON metadata
To render a reusable React component from SDL, define an instance in a JSON metadata file. This file connects the component Type to the platform’s runtime and passes any props as static or dynamic values.
The example below registers a CustomChart component and sets its title prop:
// {package}/ui/c3/src/customType/WindTurbine.CustomComponentType.json
{
"type": "CustomChart",
"title": "This title is not translated"
}Declare the SDL Type and props interface
Define a .c3typ file to describe the component’s props and link it to a render function.
The example below defines a CustomChart Type that accepts a dynamic title prop and connects to a .tsx implementation:
// {package}/ui/custom/CustomChart.c3typ
@typeScript
type CustomChart mixes ReactFunction, UiSdlComponent<UiSdlNoData> {
title: string serialized UiSdlDynamicValueSpec // Title value, supports dynamic resolution
render: ~ tsx-client // Render function lives in .tsx file
}This file connects SDL metadata to a React implementation and declares the expected props.
Implement the translated chart using props
Create the .tsx file that renders the component declared in your Type. This implementation receives props from SDL, resolves translated text using useTranslate, and renders a sunburst chart using ECharts.
The example below initializes the chart on mount, configures its layout and label formatting, and renders it with a translated title:
// {package}/ui/custom/CustomChart.tsx
import React, { useEffect, useRef } from 'react'; // Import React and lifecycle hooks
import * as echarts from 'echarts'; // Import ECharts charting library
import type { CustomChart } from '@c3/types'; // Import generated props interface
import useTranslate from '@c3/sdl-react/hooks/useTranslate';// Import translation hook
const CustomChart = ({ title }: CustomChart) => {
const myChart = useRef(null); // Create ref to attach chart to DOM
const translate = useTranslate(); // Get the translation function
useEffect(() => {
const chart = echarts.init(myChart.current); // Initialize chart in ref container
const options = {
series: [
{
radius: ['15%', '80%'], // Define inner and outer radius
type: 'sunburst', // Use a sunburst layout
sort: undefined, // Disable sorting
emphasis: { focus: 'ancestor' }, // Highlight parent segments
data: [ // Provide hierarchical chart data
{
value: 8,
children: [
{ value: 4, children: [{ value: 2 }, { value: 1 }, { value: 1 }, { value: 0.5 }] },
{ value: 2 }
]
},
{ value: 4, children: [{ children: [{ value: 2 }] }] },
{ value: 4, children: [{ children: [{ value: 2 }] }] },
{ value: 3, children: [{ children: [{ value: 1 }] }] }
],
label: {
color: '#000', // Set font color
textBorderColor: '#fff', // Add outline stroke
textBorderWidth: 2,
formatter: function (param) { // Format label based on depth
const depth = param.treePathInfo.length;
if (depth === 2) return 'radial';
if (depth === 3) return 'tangential';
if (depth === 4) return '0';
return '';
}
},
levels: [ // Define styles for each depth
{},
{ itemStyle: { color: '#CD4949' }, label: { rotate: 'radial' } },
{ itemStyle: { color: '#F47251' }, label: { rotate: 'tangential' } },
{ itemStyle: { color: '#FFC75F' }, label: { rotate: 0 } }
]
}
]
};
chart.setOption(options); // Apply chart configuration
window.addEventListener('resize', () => chart.resize()); // Handle resize events
}, []);
return (
<div>
<h1>{translate({ key: title })}</h1> {/* Translate the title */}
<div ref={myChart} style={{ width: '100%', height: '100%' }}></div> {/* Chart container */}
</div>
);
};
export default CustomChart;