C3 AI Documentation Home

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.

JavaScript
// 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.

TypeScript
// {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

TypeScript
// {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:

TypeScript
// {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:

JSON
// {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:

Type
// {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:

TypeScript
// {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;
Was this page helpful?