Last updated May 16, 2023

This section describes a Forge preview feature. Preview features are deemed stable; however, they remain under active development and may be subject to shorter deprecation windows. Preview features are suitable for early adopters in production environments.

We release preview features so partners and developers can study, test, and integrate them prior to General Availability (GA). For more information, see Forge release phases: EAP, Preview, and GA.

Comparison with UI Kit

UI Kit 2 offers enhanced features and capabilities compared to UI Kit. While much of the functionality remains similar, variations in the underlying architecture require you to make some modifications to adapt a UI kit app for compatibility with UI Kit 2.

Component APIs

Most UI Kit 2 components share the same API as their UI kit counterparts. The exceptions for some components are as follows:

FormCondition does not exist

The FormCondition component is no longer used in UI Kit 2. The same conditional rendering can be achieved with onChange handlers and state variables.

UI kit

1
2
return (
  <Form onSubmit={(data) => console.log(data)}>
    <CheckboxGroup label="More options" name="JQLOption">
      <Checkbox label="Run a JQL Query" value="true" />
    </CheckboxGroup>
    <FormCondition when="JQLOption" is={['true']}>
      <TextField label="JQL Query" name="query" />
    </FormCondition>
  </Form>
);

UI Kit 2

1
2
const [jqlOptionChecked, setJqlOptionChecked] = useState(true);

return (
  <Form onSubmit={(data) => console.log(data)}>
    <Checkbox
      label="Run a JQL Query"
      name="JQLOption"
      value={jqlOptionChecked}
      onChange={(value) => setJqlOptionChecked(value)}
    />
    {jqlOptionChecked && (
      <TextField label="JQL Query" name="query" />  
    )}
  </Form>
);

Text property no longer accepted by some components

Button, Code, StatusLozenge, and Tag components no longer accept a text property in UI Kit 2. The text content is placed in children instead. See the following examples for the Button and StatusLozenge components.

UI kit

1
2
<Button text="Click me!" onClick={() => console.log('Button clicked')} />
<StatusLozenge text="In Progress" appearance="inprogress" />

UI Kit 2

1
2
<Button onClick={() => console.log('Button clicked')}> 
 Click me!
</Button> 

<StatusLozenge appearance="inprogress">
  In Progress
</StatusLozenge> 



Hooks

Not all hooks in UI Kit 1 exist in the latest version of UI Kit.

In the latest version, implementations of standard react library hooks (useState, useEffect and useAction) can now be directly accessed from react instead (see Hooks API Reference – React). Note that the react equivalent of UI kit's useAction is useReducer, although usage of both is identical.

The useProductContext, useConfig, useContentProperty, useSpaceProperty and useIssueProperty hooks are available in our @forge/react package.

In addition, since the context/configuration/property values outputted by the @forge/react hooks take time to load, they will not be immediately available upon app mounting; i.e. these values will initially be undefined before they are loaded with actual values.

useState, useEffect, useAction (useReducer)

UI Kit 1

1
2
import ForgeUI, { useState, useEffect, useAction, Text } from '@forge/ui';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <Text>Count: {count}</Text>
  );
}

Latest version of UI Kit

1
2
import React, { useState, useEffect, useReducer } from 'react';
import { Text } from '@forge/react';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <Text>Count: {count}</Text>
  );
};

useProductContext, useConfig, useContentProperty, useSpaceProperty, useIssueProperty

See this guide for how to use them.

API calls

The @forge/api package is designed to run in a Forge function (for example, UI kit app, Custom UI resolver), and so will not work in the app frontend in UI Kit 2.

Product APIs

To make requests to product APIs, use the requestJira and requestConfluence methods exported from @forge/bridge instead.

UI kit

1
2
await api.asUser()
  .requestJira(route`/rest/api/3/issue/${issueKey}/watchers`, {
      method: "POST",
      headers: {
          "Content-Type": "application/json",
          "Accept": "application/json"
      },
      body: JSON.stringify(`${accountId}`)
  });

UI Kit 2

1
2
await requestJira(`/rest/api/3/issue/${issueKey}/watchers`, {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Accept": "application/json"
    },
    body: JSON.stringify(`${accountId}`)
});

Storage API

There is no helper method exported from @forge/bridge to call the Storage API directly. You must instead set up a Custom UI resolver that can call the Storage API, and invoke the resolver from your app frontend.

UI kit

1
2
import { storage } from '@forge/api';

const App = () => {
  const [storageValue, setStorageValue] = useState(undefined);

  return (
    <>
      <Button text="Get value" onClick={async () => {
        const value = await storage.get('example-key');
        setStorageValue(value);
      }}
      <Text>Value: {storageValue}</Text>
    </>
  );
};

UI Kit 2

frontend/hello-world/src/App.js

1
2
import React, { useState, useEffect } from 'react';
import { Text, Button } from '@forge/react';
import { invoke } from '@forge/bridge';

const App = () => {
  const [storageValue, setStorageValue] = useState(0);

  return (
    <>
      <Button onClick={async () => {
        const value = await invoke('getStorageValue', { key: `example-key` });
        setStorageValue(value);
      }}>GetValue</Button>
      <Text>Value: {storageValue}</Text>
    </>
  );
};

src/index.js

1
2
import Resolver from "@forge/resolver";
import { storage } from '@forge/api';

const resolver = new Resolver();

resolver.define("getStorageValue", async ({ payload, context }) => {
  const value = await storage.get(payload.key);
  return value;
});

export const handler = resolver.getDefinitions();



Macro Config

Defining configuration options for a macro in UI Kit 2 is now different from how it is done in UI kit. It now requires the following modifications in the manifest.yml file:

  • Replace the function property with a resource property.
  • Replace the config property value with a boolean: config: true.
  • Add the new property render, and assign its value to native: render: native.

See how to add configurations by using the Add configuration to a macro with UI Kit 2 tutorial.

UI Kit 2

1
2
modules:
  macro:
    - key: pet-facts
      resource: main # <---- replaces function: main
      render: native # <---- defines this module as a UI Kit 2 module
      title: Pet
      description: Inserts facts about a pet
      config: true   # <---- indicates that the macro extension point has a configuration panel
resources:
  - key: main
    path: frontend/hello-world/src/index.js
app:
  id: "<your app id>"
  name: pet-facts-macro

Identical in UI kit and Custom UI

1
2
import ForgeUI, { render, MacroConfig, TextField } from "@forge/ui";

const defaultConfig = {
  name: "Unnamed Pet",
  age: "0"
};

const Config = () => {
  return (
    <MacroConfig>
      <TextField name="name" label="Pet name" defaultValue={defaultConfig.name} />
      <TextField name="age" label="Pet age" defaultValue={defaultConfig.age} />
    </MacroConfig>
  );
};

export const config = render(<Config />);

Reading config values

A UI Kit 2 must read the app's config values asynchronously from the product context. The product context can be retrieved by calling view.getContext(). Note that the shape of the context object returned by the view.getContext differs slightly from the shape returned by UI kit’s useProductContext hook. See Custom UI view for the function signature.

In addition, in UI Kit 2, to hook up the configuration component, it will need to be added into the ForgeReconciler via the new addConfig function. E.g: ForgeReconciler.addConfig(<Config />);

UI kit

1
2
import ForgeUI, { useConfig, Text } from '@forge/ui';

function App() {
  const config = useConfig();

  return (
    <Text>
      Config: {config.age}
    </Text>
  );
}

UI Kit 2

app.jsx

1
2
import React, { useEffect, useState } from 'react';
import { view } from '@forge/bridge';
import { Text } from '@forge/react';

function App() {
  const [productContext, setProductContext] = useState({});
  
  useEffect(() => {
    view.getContext().then(setProductContext);
  }, []);

  return (
    <Text>
      Config: {productContext?.extension?.config?.age}
    </Text>
  );
}

config.jsx

1
2
import React from 'react';
import { TextField } from '@forge/react'; 

export const Config = () => {
  return (
    <>
      {/* Form components */}
      <TextField name="age" label="Pet age" />
    </>
  );
};

index.jsx

1
2
import React from 'react';
import ForgeReconciler from '@forge/react';
import { Config } from '../config';

ForgeReconciler.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

ForgeReconciler.addConfig(<Config />);

Logging and debugging

Due to architectural differences, the approach for logging and debugging in UI Kit 2 is different.

As UI kit runs on the server-side, any console.log statement in your app will be stored and available for viewing through the forge logs CLI command.

However, in UI Kit 2, because the app runs directly on the browser, console.log statements won't be stored and will only be available for visualization directly in your browser's developer console. If you need to log and store information, you can invoke a Resolver function and utilize console.log statements.

Rate this page: