Skip to content

Work in progress: this website is still under construction. Content may be incomplete or change at any time.

HowToCreateUiComponents

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.

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:

src/apps/empty/app.ts
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 AppUI as 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:

src/apps/empty/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-snippets for 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 (either function ComponentName ... or const 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, like div or p.

    The intl object 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 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:

src/apps/empty/AppUI.tsx
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 case props can contain a label property.

    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 onClick handler.

Now, use the new button from the AppUI component:

src/apps/empty/AppUI.tsx
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):

Buttons with click count

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 URL
import 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].

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.

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: