C3 AI Documentation Home

Use TSX in an SDL application

This guide helps developers integrate React/TypeScript components into C3's SDL ecosystem, enabling modern React development while leveraging SDL's infrastructure for routing, testing and bundling in the cloud.

Key Capabilities

  1. Create TSX pages and components in ui/c3/src/customInstances/ (container components)
  2. Build reusable React components in ui/c3/src/components/
  3. Manage state with Redux Toolkit slices in ui/c3/src/slices/ (auto-discovered)
  4. Access C3 backend APIs via SDL hooks (useC3Action, useDispatch, useSelectedState, etc.)
  5. Import existing JSON metadata components via @c3/ui/components/ComponentId

Prerequisites

  • C3 package with standard structure (see C3 Package Structure docs)
  • Redux Toolkit v2.10.1+ available in the C3 environment
  • Basic understanding of React hooks and TypeScript

Project Structure Overview

C3 packages follow a specific directory structure. Relevant folders for TSX development:

Text
myPackage/
├── myPackage.c3pkg.json          # Package manifest
├── ui/
│   └── c3/                       # UI Namespace name
│       ├── meta/                 # JSON component metadata (legacy/when needed)
│       └── src/
│           ├── customInstances/  # TSX pages (ModuleName.PageName.tsx)
│           ├── components/       # Reusable React components
│           ├── slices/           # Redux Toolkit slices (auto-discovered)
│           └── styling/          # SCSS files (recommended for performance)
├── metadata/
│   └── UiSdlRoute/              # Route definitions (CSV files)
└── src/
    └── ui/
        └── styling/              # SCSS files (legacy - requires federated modules)

Import Path Aliases

  • @c3/ui/* - Auto-generated NPM package from C3 metadata (types, hooks, federated modules)
  • @c3/app/ui/src/* - Merged ui/c3/src/ folder across all C3 packages (local bundling)
  • @c3/ui/components/* - Existing JSON/TSX components by component ID

Important - Bundling Performance

  • Recommended: Place files in ui/c3/src/ for local bundling (better performance)
  • Avoid when possible: Files in src/ become federated modules via @c3/ui/* (slower)
  • This is particularly important for SCSS files - use ui/c3/src/styling/ instead of src/ui/styling/

NPM Dependencies

When integrating existing React/NPM packages, add dependencies to the C3 runtime configuration.

Adding NPM Dependencies

JSON
// metadata/ImplLanguage.Runtime/js-webpack_c3.json
{
  "name": "js-webpack_c3",
  "npmPackages": {
    "your-new-npm-package": "1.0.0"
  }
}

Naming Conventions

Component IDs: ModuleName.ComponentName

  • ModuleName: Logical namespace (e.g., MyApp, WindTurbine, Dashboard)
  • ComponentName: Specific component/page name
  • Multiple module names can exist in one package. Overriding other packages' components is allowed.
  • Module name does NOT need to match package name

Examples

  • Package uiDemo could have: UiDemo.Home, WindTurbine.Dashboard, Analytics.Report
  • Custom instances: ui/c3/src/customInstances/MyApp.HomePage.tsx
  • Routes reference: targetModuleName=MyApp, targetPageName=HomePage

New page

Create a route and corresponding TSX custom instance.

Step 1: Define the route

Create metadata/UiSdlRoute/UiRoute.csv:

CSV
targetModuleName,targetPageName,name,urlPath
WindTurbine,Home,/wind-turbine/home,/wind-turbine/home

Route fields

  • targetModuleName - Module namespace (e.g., WindTurbine, MyApp)
  • targetPageName - Page component name (e.g., Home, Dashboard)
  • name - Route identifier (should match urlPath)
  • urlPath - URL path (e.g., /wind-turbine/home, /item/{{id}} for dynamic params)

Dynamic routes: Use {{paramName}} for URL parameters (e.g., /turbine/{{id}})

Step 2: Create the TSX custom instance

TypeScript
// ui/c3/src/customInstances/WindTurbine.Home.tsx
import React from 'react';
import { useC3Action } from '@c3/ui/UiSdlUseC3Action';
import TurbineList from '@c3/app/ui/src/components/TurbineList';

export default function WindTurbineHome() {
  const { data, status } = useC3Action({
    typeName: 'WindTurbine',
    actionName: 'fetch',
    argsArray: [{ limit: 10 }],
  });

  if (status === 'loading') return <div>Loading...</div>;
  if (status === 'error') return <div>Error loading turbines</div>;

  return (
    <div>
      <h1>Wind Turbines</h1>
      <TurbineList turbines={data?.objs || []} />
    </div>
  );
}

Component requirements

  • Must use export default
  • File name must match route: {targetModuleName}.{targetPageName}.tsx
  • Located in ui/c3/src/customInstances/

Reusable react components

Create shared components in ui/c3/src/components/ (supports nested folders).

TypeScript
// ui/c3/src/components/TurbineList.tsx
import React from 'react';

interface TurbineListProps {
  turbines: any[];
  onSelect?: (turbine: any) => void;
}

export default function TurbineList({ turbines, onSelect }: TurbineListProps) {
  return (
    <ul>
      {turbines.map((turbine) => (
        <li key={turbine.id} onClick={() => onSelect?.(turbine)}>
          {turbine.name} - {turbine.status}
        </li>
      ))}
    </ul>
  );
}

Usage in custom instances

TypeScript
// ui/c3/src/customInstances/MyApp.MyPage.tsx
import TurbineList from '@c3/app/ui/src/components/TurbineList';

// Use with props
<TurbineList turbines={data} onSelect={(t) => console.log(t)} />;

Import path: Always use @c3/app/ui/src/components/* alias (points to merged ui/c3/src/ across all packages)

Styling with SCSS

Create SCSS files in ui/c3/src/styling/ for optimal bundling performance.

SCSS
// ui/c3/src/styling/TurbineList.scss
.turbine-list {
  list-style: none;
  padding: 0;

  &__item {
    padding: 10px;
    border-bottom: 1px solid #ccc;
    cursor: pointer;

    &:hover {
      background-color: #f5f5f5;
    }
  }
}

Import in components

TypeScript
// ui/c3/src/components/TurbineList.tsx
import '@c3/app/ui/src/styling/TurbineList.scss';

export default function TurbineList({ turbines, onSelect }: TurbineListProps) {
  return (
    <ul className="turbine-list">
      {turbines.map((turbine) => (
        <li key={turbine.id} className="turbine-list__item" onClick={() => onSelect?.(turbine)}>
          {turbine.name} - {turbine.status}
        </li>
      ))}
    </ul>
  );
}

Performance note

  • Recommended: ui/c3/src/styling/ → Import via @c3/app/ui/src/styling/* (local bundling)
  • Avoid: src/ui/styling/ → Import via @c3/ui/* (federated modules, slower)

Import and render JSON components

Legacy JSON metadata components are auto-converted to React components in the @c3/ui NPM pacakge. Import by component ID using @c3/ui/components/*:

TypeScript
import WindTurbineCard from '@c3/ui/components/WindTurbine.Card';
import TurbineModal from '@c3/ui/components/WindTurbine.CreateModal';

export default function MyPage() {
  return (
    <div>
      <WindTurbineCard />
      {/* Note: Props should not be passed to imported JSON components */}
      <TurbineModal />
    </div>
  );
}

Note: Imported JSON components do not accept props. They are self-contained instances as their props are defined in the JSON.

Redux state management

Redux Toolkit slices (C3 AI Platform 8.9.0+)

Create slices in ui/c3/src/slices/ - they are auto-discovered at runtime (no imports needed).

TypeScript
// ui/c3/src/slices/turbines.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface TurbinesState {
  selectedId: string | null;
  filter: string;
}

export const turbinesSlice = createSlice({
  name: 'turbines',
  initialState: { selectedId: null, filter: '' } as TurbinesState,
  reducers: {
    selectTurbine: (state, action: PayloadAction<string>) => {
      state.selectedId = action.payload;
    },
    setFilter: (state, action: PayloadAction<string>) => {
      state.filter = action.payload;
    },
  },
});

export const { selectTurbine, setFilter } = turbinesSlice.actions;
export default turbinesSlice.reducer;

Key requirements

  • Export slice object (e.g., turbinesSlice) for action access
  • Export default reducer
  • Slice name becomes the state key (e.g., state.get('turbines'))

Using in components

TypeScript
import { useDispatch } from '@c3/ui/UiSdlUseDispatch';
import { useSelectedState } from '@c3/ui/UiSdlUseData';
import { selectTurbine } from '@c3/app/ui/src/slices/turbines';

const dispatch = useDispatch();
const turbines = useSelectedState((state) => state.get('turbines'));

// Dispatch actions
dispatch(selectTurbine('turbine-123'));

State access pattern

  • RTK slices: state.get('sliceName') - returns plain JavaScript object
  • SDL state: state.getIn(['metadata', 'components', 'byId', 'ComponentId'])?.toJS() - requires .toJS()

Immutable.js and RTK State

RTK slices are stored as plain JavaScript objects, while SDL state uses Immutable.js.

Accessing RTK slices (plain JavaScript - no .toJS() needed)

TypeScript
// In components
const turbines = useSelectedState((state) => state.get('turbines'));
// turbines is already a plain JavaScript object

// In thunks
export const myThunk = createAsyncThunk('...', async (arg, { getState }) => {
  const state = getState();
  const turbinesState = state.get('turbines');
  // turbinesState is already plain JavaScript
  const selectedId = turbinesState.selectedId; // Direct property access
});

Accessing SDL state (Immutable.js - requires .toJS())

TypeScript
// SDL state needs .toJS() conversion
const sdlData = useSelectedState((state) => state.getIn(['metadata', 'components', 'byId', 'MyComponent'])?.toJS());

Inside RTK slice reducers

TypeScript
export const turbinesSlice = createSlice({
  name: 'turbines',
  initialState: { items: [], selectedId: null },
  reducers: {
    selectTurbine: (state, action: PayloadAction<string>) => {
      // Direct property access - state is a plain object (Immer draft)
      state.selectedId = action.payload;
      state.items.push(newItem);
    },
  },
});

Why this works

  • RTK slices are stored as plain JavaScript objects in the Immutable.js Map
  • You can read them with state.get('sliceName') - no conversion needed
  • SDL state remains Immutable.js and requires .toJS() for conversion
  • RTK reducers use Immer for immutability (write code as if mutating)

Key takeaway

  • RTK slices: state.get('sliceName') returns plain JavaScript
  • SDL state: state.getIn([...])?.toJS() converts from Immutable.js
  • Inside RTK reducers: plain object with Immer (no Immutable.js)

Async logic & side effects

RTK Thunks (createAsyncThunk)

Handle async operations with Redux Toolkit thunks:

TypeScript
// In ui/c3/src/slices/turbines.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const fetchTurbines = createAsyncThunk('turbines/fetchTurbines', async (limit: number) => {
  const response = await fetch(`/api/turbines?limit=${limit}`);
  return response.json();
});

export const turbinesSlice = createSlice({
  name: 'turbines',
  initialState: { items: [], loading: false },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchTurbines.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchTurbines.fulfilled, (state, action) => {
        state.loading = false;
        state.items = action.payload;
      });
  },
});

// Usage: dispatch(fetchTurbines(10))

Note: Prefer useC3Action for C3 backend calls (see React Hooks section below).

Legacy SDL Epics (redux-observable)

Effect triggers in JSON metadata continue to work with RTK actions:

JSON
{
  "effectTriggers": [
    {
      "trigger": "turbines/selectTurbine",
      "actions": [{ "type": "WindTurbine.Panel.UPDATE", "payload": "..." }]
    }
  ]
}

Reading state in async logic

TypeScript
// RTK Thunk - access both RTK and legacy state
export const loadTurbineDetails = createAsyncThunk('turbines/loadDetails', async (id: string, { getState }) => {
  const state = getState();
  const turbinesState = state.get('turbines'); // RTK state - plain JavaScript
  const sdlState = state.getIn(['metadata', 'components']); // SDL state - Immutable.js
  // ... async logic
});

React Hooks

Data fetching - useC3Action

Recommended for C3 backend API calls. Automatically handles loading states and errors.

TypeScript
import { useC3Action } from '@c3/ui/UiSdlUseC3Action';

const { data, status, error } = useC3Action({
  typeName: 'WindTurbine',
  actionName: 'fetch',
  argsArray: [{ limit: 10 }],
});

// status: 'loading' | 'done' | 'error'

Parameters

  • typeName - C3 type name (e.g., 'WindTurbine')
  • actionName - C3 action/method name (e.g., 'fetch', 'get')
  • argsArray - Positional arguments array
    • Instance methods: [instanceOrNull, param1, param2, ...]
    • Static methods: [param1, param2, ...]

Migration tip: Replace fetch/axios calls to C3 backend with useC3Action.

Dispatch Redux actions - useDispatch

Dispatch both RTK and legacy SDL actions:

TypeScript
import { useDispatch } from '@c3/ui/UiSdlUseDispatch';
import { selectTurbine } from '@c3/app/ui/src/slices/turbines';

const dispatch = useDispatch();

// RTK slice actions
dispatch(selectTurbine('turbine-123'));

// Legacy SDL actions (plain objects)
dispatch({ type: 'WindTurbine.Modal.OPEN', payload: { id: '123' } });

Integration tip: Replace useState with Redux slices + useDispatch for shared state.

Config values - useConfig

Access C3 configuration values:

TypeScript
import { useConfig } from '@c3/ui/UiSdlUseConfig';

const myConfig = useConfig('UiSdlConfig', 'app', null, true);
const apiUrl = useConfig('UiSdlConfig', 'apiUrl', 'http://localhost');

Integration tip: Replace environment variables (import.meta.env in Vite) with useConfig.

Listen to actions - useEpic

Listen for specific Redux actions and execute side effects in components:

TypeScript
import { useEpic } from '@c3/ui/UiSdlUseEpic';

// Basic usage - listen for a specific action
useEpic(
  'FETCH_USER_DATA',
  (action, state) => {
    console.log('User data fetch initiated:', action.payload);
  },
  [],
);

// With dependencies - callback updates when userId changes
useEpic(
  'UPDATE_USER',
  (action, state) => {
    if (action.payload.userId === userId) {
      setUserData(action.payload.data);
    }
  },
  [userId],
);

// With action filter - only handle specific action variants
useEpic(
  'API_RESPONSE',
  (action, state) => {
    handleApiResponse(action.payload);
  },
  [],
  (action) => action.payload.endpoint === 'users',
);

// Listen to RTK thunk actions
useEpic(
  'turbines/fetchTurbines/fulfilled',
  (action, state) => {
    console.log('Turbines loaded:', action.payload);
  },
  [],
);

Reading Redux state - useSelectedState

Recommended for performance. Only triggers re-renders when state actually changes (not on epic-only actions):

TypeScript
import { useSelectedState } from '@c3/ui/UiSdlUseData';

// RTK slice - no .toJS() needed
const turbines = useSelectedState((state) => state.get('turbines'));

// With custom equality function
const selectedId = useSelectedState(
  (state) => state.get('turbines')?.selectedId,
  (prev, next) => prev === next,
);

// SDL state - requires .toJS()
const sdlData = useSelectedState((state) => state.getIn(['metadata', 'components', 'byId', 'MyComponent'])?.toJS());

State access

  • RTK slices: state.get('sliceName') - returns plain JavaScript
  • SDL state: state.getIn(['path', 'to', 'value'])?.toJS() - requires .toJS() conversion

Alternative

Standard useSelector from react-redux works but re-renders more often:

TypeScript
import { useSelector } from 'react-redux';

const turbines = useSelector((state) => state.get('turbines'));

Integration tip: Replace useState/useContext with useSelectedState for derived/global state.

Interoperability patterns

RTK actions → SDL Effect Triggers

RTK slice actions automatically trigger SDL effect triggers in JSON metadata:

TypeScript
// Dispatch RTK action
dispatch(selectTurbine('123'));

// JSON component can listen via effectTriggers
// "trigger": "turbines/selectTurbine"

SDL actions → RTK listeners

Listen to legacy SDL actions in RTK using extraReducers:

TypeScript
export const turbinesSlice = createSlice({
  name: 'turbines',
  initialState: {
    /* ... */
  },
  reducers: {
    /* ... */
  },
  extraReducers: (builder) => {
    builder.addMatcher(
      (action) => action.type === 'WindTurbine.Card.CLICK',
      (state, action) => {
        state.selectedId = action.payload.id;
      },
    );
  },
});

Access both state worlds

TypeScript
// In components
const dispatch = useDispatch();
const rtkState = useSelector((state) => state.get('turbines')); // Plain JavaScript
const sdlState = useSelector((state) => state.getIn(['metadata', 'components', 'byId', 'MyComponent'])?.toJS());

Memoized selectors with RTK

Use createSelector for expensive computations:

TypeScript
import { createSelector } from '@reduxjs/toolkit';

// Memoized selector - only recomputes when turbines change
const selectFilteredTurbines = createSelector([(state) => state.get('turbines')], (turbines) =>
  turbines?.items.filter((t) => t.status === 'active'),
);

// In component (with useSelectedState for best performance)
const activeTurbines = useSelectedState(selectFilteredTurbines);

Performance notes

  • useSelectedState (from UiSdlUseData) - Optimized for SDL, works with RTK. Only triggers on reducer actions. Uses shallowEqual by default.
  • useSelector (from react-redux) - Standard Redux hook. Triggers on all actions (less optimized).
  • createSelector (from RTK) - Memoizes expensive selector computations. Combine with useSelectedState for best results.
  • RTK slices are compatible with useSelectedState because they trigger state updates via reducers.

Integration Quick Reference

Common patterns when integrating existing React/Vite apps into SDL:

React/Vite PatternSDL EquivalentNotes
React Router routesCSV routes in metadata/UiSdlRoute/Define routes, create TSX custom instances
fetch('/api/data')useC3Action({ typeName, actionName, argsArray })For C3 backend APIs
Auth headers/tokensRemove - use document.cookieSDL environment provides auth automatically
useState (local)Keep useStateComponent-local state unchanged
useState (shared)Redux Toolkit slicesAuto-discovered from ui/c3/src/slices/
useContextuseSelectedState + Redux slicesGlobal state via Redux
import.meta.env.VITE_*useConfig('UiSdlConfig', 'key')Configuration values
Route paramsuseSelector(state => getPageParamFromState(state, 'id'))From @c3/ui/UiSdlConnected
CSS Modules / SCSSSCSS in ui/c3/src/styling/Import via @c3/app/ui/src/styling/* (performant)
Static assetsui/content/ folderWeb-accessible static files
NPM dependenciesmetadata/ImplLanguage.Runtime/js-webpack_c3.jsonThen run Js.upsertRuntime() in C3 console

Build/Dev

  • No separate build commands - C3 VSCode extension handles bundling
  • Redux store automatically created and available (no Provider setup needed)
  • Hot reload via C3 extension

Performance Best Practices

  • Place UI code in ui/c3/src/* for local bundling
  • Avoid src/* for UI files (creates federated modules via @c3/ui/*)
  • This integration approach leverages SDL bundling while preparing for future phases
Was this page helpful?