HowToCreateUiComponents
How to create UI components
Section titled “How to create UI components”When developing applications with the Open Pioneer Trails client framework, React (opens in a new tab) can be used to create UI components. We can also use pre-defined components from the Chakra UI (opens in a new tab) framework.
Documentation
Section titled “Documentation”- React’s official documentation (opens in a new tab)
- Chakra’s component overview (opens in a new tab) (with lots of examples)
Getting started
Section titled “Getting started”In this tutorial, we will take the empty app (src/apps/empty) as a starting point.
Start your dev server (by running pnpm dev) and open the empty app in your browser (usually at http://localhost:5173/sites/empty/ (opens in a new tab)).
The app’s main entry point (app.ts) creates and defines a custom element class (from the Web Component standard, see MDN (opens in a new tab)), including the app’s user interface:
import { createCustomElement } from "@open-pioneer/runtime";import * as appMetadata from "open-pioneer:app";import { AppUI } from "./AppUI"; // (1)
const Element = createCustomElement({ component: AppUI, // (2) appMetadata});
customElements.define("empty-app", Element);- (1)
Imports
AppUI(a React component) from the given module. - (2)
Specifies
AppUIas the application’s UI. Any kind of React component can be used in this place.
When the application is instantiated, the AppUI component will be rendered automatically.
React components are typically developed in .jsx or .tsx files, which are JavaScript/TypeScript modules with JSX (opens in a new tab) syntax extensions.
In this case, the app’s entire UI is defined in AppUI.tsx:
// (1)import { Container, Heading, Text, chakra } from "@chakra-ui/react";import { useIntl, useService } from "open-pioneer:react-hooks";import { Greeter, SimpleUiComponent } from "sample-package";
// (2)export function AppUI() { const intl = useIntl(); const greeter = useService<Greeter>("sample-package.Greeter"); // (3) return ( <Container> <Heading as="h1" size="lg"> {intl.formatMessage({ id: "heading" })} </Heading> <Text pt={5}>{intl.formatMessage({ id: "text" })}</Text> <Text pt={5}> This messages comes from the sample package{"'"}s greeter service: {greeter.greet()} </Text> <chakra.div mt={5}> <SimpleUiComponent textToShow="This text is rendered inside the sample UI-Component 'SimpleUiComponent'"></SimpleUiComponent> </chakra.div> </Container> );}-
(1) Imports Chakra components used by the app’s UI. Remember to use
@open-pioneer/chakra-snippetsfor imports of chakra snippets (not used in this example). -
(2) Defines a React component called
AppUI. (Almost) all React components should be defined as functions (eitherfunction ComponentName ...orconst ComponentName = ...); -
(3) This defines the content of the component. In this case, Chakra components (e.g.
Container,Text) are used. You can also use plain html elements, likedivorp.The
intlobject here is used to translate messages into multiple languages (see How to translate an App). We will not dive into internationalization (i18n) in this tutorial and use static strings instead.
Defining new UI components
Section titled “Defining new UI components”Defining a new React component is as simple as creating a new function.
For this example, we will create a button that has a label and tracks the number of times it has been clicked:
import { Container, Heading, Text, chakra, Button } from "@chakra-ui/react";import { useState } from "react";
// export function AppUI ...
interface ClickableButtonProps { label: string;}
// (1)function ClickableButton({ label }: ClickableButtonProps) { // (2) const [clickCount, setClickCount] = useState(0); const incrementClickCount = () => { setClickCount(clickCount + 1); };
// (3) const clickText = clickCount === 0 ? "" : `(${clickCount})`; return ( <Button onClick={incrementClickCount}> {label} {clickText} </Button> );}-
(1) Defines the new component. React components should start with an uppercase letter and take a single parameter (called
props); in this casepropscan contain alabelproperty.Note In simple (local) components like this, you could also inline the type definition of
ClickableButtonProps. If you have many properties, or intend to share your components across modules or packages, using a separate type is strongly recommended. -
(2) Uses the
useState(opens in a new tab) hook to remember the number of types the button has been clicked. -
(3) Renders the button with the appropriate text and wires up the
onClickhandler.
Now, use the new button from the AppUI component:
import { Container, Button, VStack } from "@chakra-ui/react";import { useState } from "react";
export function AppUI() { return ( <Container> <VStack> <ClickableButton label="First" /> <ClickableButton label="Second" /> </VStack> </Container> );}
// interface ClickableButtonProps ...The final result looks like this (after a clicking a few times):

Embedding images
Section titled “Embedding images”Images or other static resources from your app or package can usually be imported directly into your source code.
For example, to embed an image called my-image.png from the same directory as your React component:
import { Image } from "@chakra-ui/react";
// Import image as URLimport myImageUrl from "./my-image.png";
// Later, use the URL in your code<Image src={myImageUrl} />;This approach works well if you only care about the assets data, and not its file name or location. It relies on Vite’s support for static assets (opens in a new tab), which will automatically bundle the referenced files as assets of your application.
For different approaches and more details, see Including static assets.
Defining a UI Component in a different module
Section titled “Defining a UI Component in a different module”Since react components are simple functions (or sometimes classes), they can be simply be moved into another file in combination with the usual import and export keywords.
Moving a component into a shared package is just as simple: just export the component (and its props type, if using TypeScript) from the package, for example from the index.ts[x].
Interacting with the rest of the system
Section titled “Interacting with the rest of the system”React props are a powerful system that can handle arbitrary values such as strings (like in the example above), but also functions (e.g. event handlers or render props (opens in a new tab)).
However, props work only from parent to child, which can be tedious with deeply nested component trees.
React has a second system (the Context API (opens in a new tab)) that can be used to inherit values into all children (and their children etc.) of a certain component.
This can be extremely powerful but also makes components harder to understand.
We have implemented hooks such as useService and useIntl (built on top of the context API) to allow React components to interact with the rest of the framework (see How to use a service and How to translate an app).
It is best to compose a UI from a set of simple, reusable React components that mostly rely on props and hooks.
This makes them easier to understand, to reuse and to test in isolation.
More powerful mechanisms should only be used when those simple approaches are not sufficient anymore.
Keeping shared state
Section titled “Keeping shared state”Like mentioned above, simple state can often be managed through props and component-local state.
Plain services (together with events) can be used to keep state that is used in multiple places (components, services) of the application.
To manage more complex state, React developers usually reach for a state management library (e.g. Redux or Zustand). We have implemented a custom state management solution based on signals (see Reactivity-API (opens in a new tab), Trails bindings for React (opens in a new tab)). This API is used for reactive interfaces provided by Trails packages and will probably fit your needs as well. A relatively complete example is available here (opens in a new tab).
Other third party libraries that work well, too: