# Create an app card Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card Learn how to create an app card on the latest version of the developer platform. You can create a React-based app card for projects on the latest versions (`2025.2` and `2026.03`) of the developer platform. App cards work similarly to existing cards you may have [built for legacy apps](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference), with some minor modifications to your configuration files. This guide walks you through how to create a boilerplate app card in an existing app, along with how to upload and preview your card in HubSpot. You'll then upload all these files to your project, where you can preview the card in a developer test account where you've installed your app. ## Prerequisites * If you haven't done so yet, [create an app](/docs/getting-started/quickstart). * It's recommended to [create a configurable test account](/docs/developer-tooling/local-development/configurable-test-accounts) so that you can build and test in an isolated environment. * Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). ## Create an app card To add a new app card component to your project, use the terminal to navigate into your local project directory, then run the following command: ```shell theme={null} hs project add ``` Then, when prompted to select a component to add, select **Card**. ![Screenshot of the terminal prompt for selecting the type of component to add](https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/developer-local/terminal-prompt-select-an-app-component.png) If your project doesn't contain any app cards yet, a `cards/` directory will be created for you in the `app/` directory. The `cards/` directory will contain: * A JSON card configuration file (`*-hsmeta.json`) * A React component file (`.jsx`) * A `package.json` file If your project already includes app cards, the above files will be added to the existing `cards/` directory (excluding `package.json` if it already exists). ```shell theme={null} myProject └── src/ └── app/ └── cards/ ├── NewCard-hsmeta.json ├── NewCard.jsx └── package.json ``` Learn more about these files in the [app cards reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference). To upload the new card to your HubSpot account: * Run `hs project install-deps` to install the dependencies needed for the app card. This will update the `cards/` directory with the necessary Node modules and a `package-lock.json` file, which will speed up the build of the uploaded app card extension and ensure that any dependencies in your local development environment and production match. * Then, run `hs project upload` to upload the project to your default account. * To specify a different account (such as a separate test account), include the `--account` flag and specify the HubID of the account. For example `hs project upload --account 123456`. * If the project hasn't been uploaded before, you'll be prompted to confirm that you want to create the project in your account. Otherwise, the terminal will display the build and deploy status, then confirm once the project has been successfully uploaded. ## View the card in HubSpot After uploading the project, you can view it in HubSpot by running `hs project open`. A browser tab will open to the project details page, where you can view your project, app, and its new card component. Screenshot of the project details page in HubSpot showing an app with a card component If you haven't yet installed the app in the account, you'll need to do so before you can view the card. To install the app: * Click the **name** of the app in the project summary on the left, or under *Project components*. Screenshot of the app on the project details page in HubSpot * Click the **Distribution** tab. * Click **Install now**. Screenshot of the install now button on the app distribution tab The boilerplate app card created by `hs project add` is configured to appear in the middle column of contact records. To display the card, you'll first need to add it to the contact record view: * In your HubSpot account, navigate to **CRM** > **Contacts**. * Click the **name** of a contact. * At the top of the middle column of the contact record, click **Customize**. Screenshot of the customize button on the contact timeline * Click **Default view**. * Select the **tab** that you want to add the card to. You can then hover over the location where you want to place the card and click the **plus button**. This can be adjusted at any time after initial setup. Screenshot showing the plus button to add a card to the contact record view * In the right sidebar, click the **Card library** tab. Then, click the **Card types** dropdown menu and select **App** to filter for app cards. Screenshot showing the apps filter in the card library * Click **Add card** under the app card you created, then click the **close** button in the top right of the sidebar. * In the top right, click **Save and exit**. You'll then be redirected back to the contact record where your card will now appear. Keep the contact record page open in your browser for the next step, when you'll start the local development server. ## Start local development With your app card added to all contact records, you can now continue to build the app card. The easiest way to quickly iterate is to start the local development server with the `hs project dev` command: * In the terminal, run `hs project dev`. * Follow the terminal prompts to select the **account** you want to use for local development. * The terminal will start the local development server, then confirm once it's running. * With the server running, navigate back to the browser tab with the contact record and reload the page. The app card will display with a `Developing locally` tag, indicating that the local development server is ready. Screenshot showing the app card rendered on the contact timeline with the Developing Locally tag The local development server will automatically detect any changes saved to the app card's front-end React files (i.e., any `.jsx` or `.tsx` files). If you need to make changes to other file types, such as a `.json` configuration file, you'll need to reupload the project and restart the local development server. ## Next steps Check out the following resources to build out the card's appearance and functionality. * Consult the [UI extension component reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to add more UI components to the card. * See all app card configuration options and more in the [app card reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference). * View all available utilities and methods available for UI extensions in the [UI extension SDK reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk). * Learn how to [fetch data for your app card](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data) with `hubspot.fetch()`. * To list an app with app cards on the HubSpot marketplace, follow the [app card review process steps below](#submitting-app-cards-to-the-hubspot-marketplace). ### Migrating a previously created app card If you need to migrate an existing project with app cards to the developer platform, check out the following guides: * [Migration overview](/docs/apps/developer-platform/build-apps/migrate-an-app/overview) * [Migrate an app to 2026.03](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-to-the-latest-platform-version) If your existing project had a card with custom objects connected, confirm that you have the `crm.objects.custom.read` scope on your app before migration. For projects built before version `2025.2`, custom object cards could be built with only the `crm.schemas.custom.read` scope required. In the latest versions (`2025.2` and `2026.03`) of the developer platform, `crm.objects.custom.read` is required. If you don't include this scope, the build during the migration will fail with the following error: **\[ERROR] Build failed or timed out. Inspect the failure to update your build and retry the migration.** Your customer-facing app will be unaffected, despite the status in the project details page. To fix the issue, add the `crm.objects.custom.read` scope to your legacy app's scopes, re-build the app on version `2023.2`, then re-try the migration. ### Submitting app cards to the HubSpot Marketplace To submit your app cards for HubSpot Marketplace approval, fill out [this form](https://96it.share.hsforms.com/2a_JwN0iMSDuqoDikVh255Q) with the required information below: * **Production app ID:** the ID of the app you intend to update or list on the HubSpot Marketplace. * **Build ID:** the build ID of the app installed in the test account. * **Testing video URL:** a short video with detailed instructions of how users will interact with the card. For guidance on recording a sufficient video, refer to the [app certification demo video requirements](/docs/apps/developer-platform/list-apps/apply-for-certification/applying-for-app-certification#requirements-for-the-app-demo-video). * **App status:** indicate if this is a new app or an app already on the marketplace. After receiving the above information, the HubSpot Ecosystem Quality team will review your app cards and share initial feedback within 10 business days. Please action this feedback as soon as you receive it, as multiple reviews may be necessary for final approval. Once the review is successfully completed and your app card is approved, you will receive confirmation with the CTA to resubmit your draft listing. Please do not resubmit your listing before receiving confirmation of approval from [app-card-review@hubspot.com](mailto:app-card-review@hubspot.com). Any resubmissions of your listing before the app card review is completed will be rejected. # App cards overview Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview An overview of building cards to customize the HubSpot UI. Add a layer of UI customization to your app by including app cards that can display data, allow users to perform actions, and more. You can build app cards that display on CRM records, preview panels, the help desk workspace, and the sales workspace. Once the app is installed in an account, Super Admins and users with *Customize record page layout* permissions can [add any included app cards](https://knowledge.hubspot.com/integrations/install-and-manage-app-cards) to their configured locations in HubSpot. ![Screenshot of a custom app card in the middle column of a contact record](https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/developer/contact-record-custom-app-card-example.png) ## Sensitive Data scopes Apps that use [Sensitive Data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) can include app cards, but with the following restrictions: * Apps with Sensitive Data scopes **cannot** use [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data), preventing Sensitive Data from being sent to external services. * Apps with Sensitive Data scopes **cannot** use serverless functions. If your app requires both Sensitive Data scopes and `hubspot.fetch()`, you'll need to use separate apps for each capability. **Please note:** these restrictions are safeguards provided by HubSpot, but they are not a substitute for your own Sensitive Data handling practices. As a developer, you are responsible for ensuring that your app handles Sensitive Data in compliance with applicable laws, regulations, and HubSpot's [Sensitive Data terms](https://legal.hubspot.com/sensitive-data-terms). To get started building app cards: * Follow the [quickstart guide](/docs/getting-started/quickstart) to start with a new app, or check out the [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app) to customize the features and configuration of a new app. * Migrate an existing legacy [private](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-private-app) or [public](/docs/apps/developer-platform/build-apps/migrate-an-app/migrate-an-existing-public-app) app to the developer platform. * Learn how to [add an app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) to an existing developer platform app. * Review the [app card reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) for a full breakdown of app card options and features. If you plan on distributing your app on the HubSpot Marketplace, any app cards you've built are subject to a technical review from the HubSpot Ecosystem Quality team, and must adhere to the requirements listed [here](/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements#app-card-requirements). # App cards reference Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference Reference information for building app cards on the latest version of the developer platform. Below, find reference information for building app cards ## Project structure To add an app card to an app, create a `cards` directory within `src/app`. The `cards` directory should contain: * A JSON configuration file for each card that defines the card's schema (`*-hsmeta.json`). * A React file for each card that renders the card's front-end (`.jsx` or `.tsx`). * A `package.json` file to handle any needed dependencies. This file is shared between all app cards. ```shell theme={null} project-folder/ └── src/ └── app/ ├── app-hsmeta.json └── cards/ └── my-app-card-hsmeta.json └── my-app-card.jsx └── package.json ``` ## App card configuration In the `*-hsmeta.json` configuration file for your app card, include the properties below. ```json theme={null} { "uid": "example-card", "type": "card", "config": { "name": "Hello Example App", "description": "A description of the card's purpose.", "entrypoint": "/app/cards/ExampleCard.jsx", "objectTypes": ["contacts"] } } ```

Fields marked with \* are required.

| Field | Type | Description | | ---------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `uid`\* | String | The card's unique identifier. This can be any string, but should meaningfully identify the card. HubSpot will identify the card by this ID so that you can change the card's title without removing historical or stateful data, such as the position on the CRM record. | | `type` | String | The type of component, which should be `card` in this case. | | `config` | Object | An object containing configuration details. | | `name`\* | String | The card's title, as displayed in HubSpot's UI. | | `description` | String | A description of the card. | | `previewImage` | Object | An object containing the `file` and `altText` fields. The `file` field is the relative path to the preview image. Valid file extensions are png, jpeg, jpg, or gif. The maximum file size is 5.0 MB. The `altText` field is a short description of the image. | | `entrypoint`\* | String | The file path of the card's front-end React code. | | `location` | `crm.record.tab` \| `crm.record.sidebar` \| `crm.preview` \|`helpdesk.sidebar` | The surface this card is restricted to. If omitted, the card is eligible for every surface that's compatible with its card type and `objectTypes`. See [*Card locations*](#card-locations) for accepted values. | | `objectTypes`\* | Array | The types of CRM records that the card will appear on. See the [*Supported objects*](#supported-objects) section below for more details. | ### Supported objects In the `objectTypes` array of the card's `*-hsmeta.json` configuration file, specify the types of CRM records that the card will appear on. Below are the currently supported CRM objects, their `objectType` value, and the minimum [scope](/docs/apps/legacy-apps/public-apps/overview#scope-types) to add to your app. For HubSpot's standard objects, `objectType` values are not case sensitive, and both the singular and plural are supported. For example, `"CONTACT"` and `"contacts"` are both valid. | CRM object | objectType value | Related scope | | ---------------- | ------------------------------- | -------------------------------------------------------------------------------------------- | | Contacts | `CONTACT` | `crm.objects.contacts.read` | | Companies | `COMPANY` | `crm.objects.companies.read` | | Deals | `DEALS` | `crm.objects.deals.read` | | Tickets | `TICKETS` | `tickets` | | Orders | `ORDERS` | `crm.objects.orders.read` | | Carts | `CARTS` | `crm.objects.carts.read` | | Invoices | `INVOICES` | `crm.objects.invoices.read` | | Leads | `LEADS` | `crm.objects.leads.read` | | Marketing events | `MARKETING_EVENTS` | `crm.objects.marketing_events.read` | | Partner accounts | `PARTNER_ACCOUNTS` | `crm.objects.partner-accounts.read` | | Partner clients | `PARTNER_CLIENTS` | `crm.objects.partner-clients.read` | | Partner services | `PARTNER_SERVICES` | `crm.objects.partner-services.read` | | Payments | `COMMERCE_PAYMENTS` | `crm.objects.commercepayments.read` | | Products | `PRODUCTS` | `crm.objects.products.read` | | Quotes | `QUOTES` | `crm.objects.quotes.read` | | Subscriptions | `SUBSCRIPTIONS` | `crm.objects.subscriptions.read` | | Custom objects | `p_objectName` (case sensitive) | `crm.objects.custom.read` | | App objects | `app_object_uid` | See [app objects scopes](/docs/apps/developer-platform/add-features/app-objects/reference#scopes) | The custom object wildcard `p_*` can be used in lieu of `p_objectName` to target all custom objects in the HubSpot account. In addition, the following CRM objects are supported if they've been [activated in the data model builder](https://knowledge.hubspot.com/data-management/use-the-data-model-builder): | CRM object | objectType value | Related scopes | | ------------ | ---------------- | ------------------------------- | | Appointments | `APPOINTMENTS` | `crm.objects.appointments.read` | | Courses | `COURSES` | `crm.objects.courses.read` | | Listings | `LISTINGS` | `crm.objects.listings.read` | | Services | `SERVICES` | `crm.objects.services.read` | | Projects | `PROJECTS` | `crm.objects.projects.read` | ### Card locations By default, when `location` is omitted, the card is eligible to appear on every surface that's compatible with its card type and `objectTypes`. Account admins decide where it actually appears via the [page editor](https://knowledge.hubspot.com/object-settings/customize-records). To restrict your card to a single surface, set `location` to one of the supported values: ```json theme={null} { "uid": "example-card", "type": "card", "config": { "name": "Hello Example App", "description": "A description of the card's purpose.", "location": "crm.record.tab", "entrypoint": "/app/cards/ExampleCard.jsx", "objectTypes": ["contacts"] } } ``` **Please note:** omitting `location` does not override the `objectTypes` constraint. For example, a card with `"objectTypes": ["TICKETS"]` is still scoped to ticket-bearing surfaces, regardless of whether `location` is set. Below are the currently supported locations. * `crm.record.tab`: places the extension in the middle column of CRM record pages, either in one of HubSpot's default tabs or in a custom tab. When `objectType` is set to `COMPANIES`, the card will also be available in the [sales workspace target accounts preview panel](https://knowledge.hubspot.com/prospecting/manage-companies-in-the-sales-workspace). If you've customized the middle column previously, you'll need to [customize the middle column view](https://knowledge.hubspot.com/object-settings/customize-records) to make any newly created extensions visible. ![middle-column-example-card](https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/middle-column-example-card.png) * `crm.record.sidebar`: displays the extension in the right sidebar of CRM record pages. Extensions in the sidebar cannot use [CRM data components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview). When `objectType` is set to `DEALS`, the card will also be available in the [sales workspace deals sidebar](https://knowledge.hubspot.com/prospecting/create-and-manage-deals-in-the-sales-workspace). ![right-sidebar-example-card](https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/right-sidebar-example-card.png) * `crm.preview`: displays the app card in the preview panel that you can access throughout the CRM. When using this location, the extension will be available when previewing the `objectTypes` specified in the [JSON config file](#example-card-json). This includes previewing records from within CRM record pages, index pages, board views, and the lists tool. Learn more about [customizing previews](https://knowledge.hubspot.com/object-settings/customize-record-previews). ![preview-example-card](https://knowledge.hubspot.com/hubfs/Knowledge_Base_2023_2024/preview-example-card.png) * `helpdesk.sidebar`: displays the card in the ticket sidebars within help desk. This includes both the ticket preview panel on the help desk home page and the right sidebar of the ticket view in help desk. To add a card to this location, you'll need to [configure your help desk settings](https://knowledge.hubspot.com/help-desk/customize-the-right-sidebar-of-help-desk) to include the card. When creating an extension for this location, you'll also need to ensure that the [app's JSON configuration file](/docs/apps/developer-platform/build-apps/app-configuration) includes `tickets` in the `scopes` array, and that the [card's JSON configuration file](#app-card-schema) includes `tickets` in the `objectTypes` field. * Help desk home: ![help-desk-uie-example-home-page-preview](https://www.hubspot.com/hubfs/Knowledge_Base_2023_2024/help-desk-uie-example-home-page-preview.png) * Help desk ticket view: ![help-desk-uie-example-ticket-page](https://www.hubspot.com/hubfs/Knowledge_Base_2023_2024/help-desk-uie-example-ticket-page.png) ## Building the React front-end The UI of an app card is created by a React component file, either `.jsx` or `.tsx`. This file lives in the `cards/` directory alongside the [card configuration JSON file](#app-card-schema) (`*-hsmeta.json`). In the card configuration file, you'll specify the path of the React file in the `entrypoint` field. Below is an example of a simple app card, which includes `Text` and `Button` [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to render the card content, along with a `Flex` component for managing layout. ```jsx theme={null} import React from "react"; import { Text, Button, Flex, hubspot } from "@hubspot/ui-extensions"; // Define the extension to be run within the Hubspot CRM hubspot.extend(() => ); // Define the Extension component const Extension = () => { return ( This is a simple getting started UI extension with static text. ); }; ``` The following reference documentation is provided for building out card appearance and functionality: * [UI extension components reference documentation](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) * [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk) ## Managing dependencies You can include dependencies for your app card in a `package.json` file within the `cards/` directory. By default, when adding an app card through the `hs project add` command, a `package.json` file will be created for you with the following dependencies: * `@hubspot/ui-extensions` * `react` * `typescript` To install dependencies for project components with a `package.json` file, you can run the `hs project install-deps` command in your project directory. ```json theme={null} { "name": "hubspot-example-extension", "version": "0.1.0", "license": "MIT", "dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" }, "devDependencies": { "typescript": "^5.3.3" } } ``` # Create an app home page Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-home-page Learn how to create a home page for your app. [App pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview) are a newer extension point and provide additional functionality compared to the app home page feature described in this article. While app home pages are still supported, it's recommended you try out app pages to leverage new components and features such as page routing and passing parameters between pages. App home pages let you create a custom landing experience for users when they navigate to your app in HubSpot. Built with React and powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk), home pages are built with the same toolkit available for [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) and [settings pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), including HubSpot's UI components and data fetching utilities. Below, learn how to create an app home page on the latest versions of the developer platform (`2025.2` and `2026.03`). ## Prerequisites * If you haven't done so yet, [create a new app](/docs/getting-started/quickstart). * Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). ## Create an app home page To add a home page component to an existing app, use the terminal to navigate into your local project directory, then run the following command: ```shell theme={null} hs project add ``` Then, when prompted to select a component to add, select **Home**. ![Screenshot of the terminal prompt for selecting the type of component to add](https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/developer/hs-project-add-home.png) A new `pages/` directory will be created in the project's `src/app/` directory. The `pages/` directory will contain: * A JSON configuration file (`*-hsmeta.json`) * A React component file (`.tsx`) * A `package.json` file ```shell theme={null} myProject └── src/ └── app/ └── pages/ ├── home-hsmeta.json ├── Home.tsx └── package.json ``` ```json theme={null} { "uid": "app-home", "type": "page", "config": { "entrypoint": "/app/pages/Home.tsx", "location": "home" } } ``` ```tsx theme={null} import React from "react"; import { EmptyState, Text, hubspot } from "@hubspot/ui-extensions"; import { HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, } from "@hubspot/ui-extensions/pages/home"; hubspot.extend<"home">(({ context }) => { return ; }); const NewHomesPage = ({ context }) => { return ( <> console.log("P1")}>Primary 1 console.log("S1")}>Secondary 1 console.log("S2")}>Secondary 2 Build your application home page here! ); }; ``` ```json theme={null} { "name": "hubspot-example-extension", "version": "0.1.0", "license": "MIT", "dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" }, "devDependencies": { "typescript": "^5.3.3" } } ``` To upload your home page to HubSpot: * Run `hs project install-deps` from within your local project directory to install necessary dependencies. This will create a `package-lock.json` file, which will speed up the project build time and ensure that any dependencies in your local development environment and production match. * Then, run `hs project upload`. * After the project finishes deploying, open the project in HubSpot by running `hs project open`. Alternatively, in HubSpot you can navigate to **Development** > **Projects**, then click the **name** of your project. * Your home component should now be listed on the details page. Screenshot of the home component on project details page ## View the app home page in HubSpot To verify the home page is working correctly: * In the HubSpot account where you've installed the app, click the **Marketplace** icon. * In the *Your recently visited apps* section, click the **name of the app** to open the app's overview page. Screenshot of the HubSpot Marketplace dropdown app menu where you can select a recently visited app If your app doesn't appear in the dropdown menu, or if you have more than three apps installed, you can access your app home page directly at the following URL: `https://app.hubspot.com/app/{HubID}/{appId}` Screenshot of an app's settings page in HubSpot. You can now continue to build out your app's home page as needed. Similar to building app cards, you'll use the UI extensions SDK to add functionality and visual elements to your home page. Note that all the existing limitations around building UI extensions apply to building a settings page. * Use `hubspot.fetch` to leverage your backend to save and retrieve settings. Learn more about using this approach in the legacy documentation. * Check out the reference documentation on [standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for how to use React components when building your extension, or use the component in the [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit). * Use the `hs project dev` command to iteratively build out your settings page and preview your changes locally. The local development server will only pick up changes saved to the front-end React file. If you update the `*-hsmeta.json` or `package.json` files, you'll need to stop the server, upload your changes, then start the server again. ## Home page components In addition to the full set of [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview), there are three UI components specific to app home pages that you can use together to add action buttons to the header area: * `` * `` * `` To use these components, you'll need to be on version `0.10.0` or later of the `ui-extensions` NPM package. You can check your current version by running `npm list` or `npm list -g`, and install the latest version by running `npm i @hubspot/ui-extensions`. These components imported from `@hubspot/ui-extensions/pages/home`. Screenshot of the app home screen featuring the header action buttons ```tsx theme={null} import { HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, } from "@hubspot/ui-extensions/pages/home"; console.log("P1")}>Primary console.log("S1")}>Secondary 1 console.log("S2")}>Secondary 2 ; ``` Below, learn more about each component. ### HeaderActions The `HeaderActions` component is the main wrapper component for the primary and secondary header action button components. It's recommended to only include one instance of `HeaderActions` in your app, and to avoid adding/removing it dynamically which can result in unexpected behavior. Only `PrimaryHeaderActionButton` and `SecondaryHeaderActionButton` are supported as children. ```jsx theme={null} import { HeaderActions, PrimaryHeaderActionButton } from "@hubspot/ui-extensions/pages/home"; Primary button ; ``` ### PrimaryHeaderActionButton The `PrimaryHeaderActionButton` component renders an orange button, and should be used for the primary action on the home page. `HeaderActions` can contain only one instance of `PrimaryHeaderActionButton`. For additional buttons, use `SecondaryHeaderActionButton`. Screenshot of a primary header action button ```jsx theme={null} import { HeaderActions, PrimaryHeaderActionButton } from '@hubspot/ui-extensions/pages/home'; // Basic primary button saveData()}> Save changes // Primary button as link View details ``` | Prop | Type | Description | | --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `children` (required) | ReactNode | The button text or content. | | `onClick` | `() => void` | A function that will be invoked when the button is clicked. It receives no arguments and it’s return value is ignored. | | `href` | String \| Object | Include this prop to open a URL on click. Contains the following fields:
  • `url` (string): the URL that will open on click.
  • `external` (boolean): set to `true` to open the URL in a new tab and display an external link icon. By default:
    • Links to HubSpot app pages will open in the same tab and will not include an icon.
    • Links to non-HubSpot app pages will open in a new tab and include the icon.
When a button includes both `href` and an `onClick` action, both will be executed on button click. | | `disabled` | Boolean | When set to `true`, the button will render in a greyed-out state and cannot be clicked. | | `overlay` | Object | Include a [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) or [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel) component in this object to open it as an overlay on click. Learn more about [using overlays](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#open-overlays). | ### SecondaryHeaderActionButton The `SecondaryHeaderActionButton` component renders additional buttons that appear in an *Actions* dropdown menu next to the primary button. You can include multiple secondary buttons, and each will appear as another item in the *Actions* dropdown menu. Screenshot of several secondary action buttons ```jsx theme={null} import { HeaderActions, PrimaryHeaderActionButton, SecondaryHeaderActionButton, } from "@hubspot/ui-extensions/pages/home"; hubspot.extend < "home" > (() => { return ; }); function AdvancedHeaderActions() { const [isSaving, setIsSaving] = useState(false); const handleSave = async () => { setIsSaving(true); try { console.log("Saving..."); } finally { setIsSaving(false); } }; return ( {isSaving ? "Saving..." : "Save Changes"} console.log("Export Data")}>Export Data console.log("Share")}>Share Help docs ); } ``` # Create app pages Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages Learn how to create custom pages for your app with routing support. App pages let you create custom page experiences for your app in HubSpot, including a home page and additional pages. Built with React and powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk), app pages are built with the same toolkit available for [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card) and [settings pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), including HubSpot's UI components and data fetching utilities. With app pages, you can create multiple pages within your app and navigate between them. Users can access your app's home page directly from the HubSpot Marketplace menu, and you can link to additional pages within your app. Below, learn how to create app pages on the latest versions of the developer platform (`2025.2` or `2026.03`). ## Prerequisites * Ensure you've installed the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). * If you haven't done so yet, [create a new app](/docs/getting-started/quickstart). ## Create app pages ### Add an app pages component to your project To add an app pages component to an existing app, use the terminal to navigate into your local project directory, then run the following command: ```shell theme={null} hs project add ``` Then, when prompted to select a component to add, select **Pages**. A new `pages/` directory will be created in the project's `src/app/` directory. The `pages/` directory will contain: * A JSON configuration file (`pages-hsmeta.json`) * A React component file (`Pages.tsx`) * A `package.json` file ```shell theme={null} myProject └── src/ └── app/ └── pages/ ├── pages-hsmeta.json ├── Pages.tsx └── package.json ``` ```json theme={null} { "uid": "my-app-pages", "type": "page", "config": { "entrypoint": "/app/pages/Pages.tsx" } } ``` ```tsx theme={null} import React from "react"; import { Text, hubspot } from "@hubspot/ui-extensions"; import { createPageRouter, PageHeader, PageRoutes, PageLink, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages"; const AppHomePage = () => { return ( <> Home Home Build your application home page here! ); }; const PageLayout = ({ children }) => { return ( <> Home Documentation {children} ); }; const PageRouter = createPageRouter( ); hubspot.extend<"pages">(() => ); ``` ```json theme={null} { "name": "hubspot-example-extension", "version": "0.1.0", "license": "MIT", "dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" }, "devDependencies": { "typescript": "^5.3.3" } } ``` ### Upload to HubSpot To upload your app pages to HubSpot: * Run `hs project install-deps` from within your local project directory to install necessary dependencies. This will create a `package-lock.json` file, which will speed up the project build time and ensure that any dependencies in your local development environment and production match. * Then, run `hs project upload`. * After the project finishes deploying, open the project in HubSpot by running `hs project open`. Alternatively, in HubSpot you can navigate to **Development** > **Projects**, then click the **name** of your project. * Your pages component should now be listed on the details page. Screenshot of the pages component on project details page ## View app pages in HubSpot To verify your app pages are working correctly: * In the HubSpot account where you've installed the app, click the **Marketplace** icon. * In the *Your recently visited apps* section, click the **name of the app** to open the app's home page. Screenshot of the HubSpot Marketplace dropdown app menu where you can select a recently visited app If your app doesn't appear in the dropdown menu, or if you have more than three apps installed, you can access your app's home page directly at the following URL: `https://app.hubspot.com/app/{HubID}/{appId}` Screenshot of an app's home page in HubSpot. You can now continue to build out your app pages as needed. Similar to building app cards, you'll use the UI extensions SDK to add functionality and visual elements to your pages. Note that all the existing limitations around building UI extensions apply to building app pages. * Use `hubspot.fetch` to communicate with your backend to save and retrieve data. * Check out the reference documentation on [standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for how to use React components when building your extension, or use the component in the [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit). * Use the `hs project dev` command to iteratively build out your app pages and preview your changes locally. The local development server will only pick up changes saved to the front-end React file. If you update the `*-hsmeta.json` or `package.json` files, you'll need to stop the server, upload your changes, then start the server again. ## Next steps Now that you've created your app pages, check out the following articles for guidance as you continue to develop your app page: * [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing) * [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking) * [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference) * [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header) * [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes) # App pages overview Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview An overview of building pages to customize the HubSpot UI. App pages are a newer extension point and provide additional functionality compared to the older [app home page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-home-page) feature. While app home pages are still supported, it's recommended you try out app pages to leverage new components and features such as page routing and passing parameters between pages. Create custom page experiences for your app in HubSpot with app pages. Built with React and powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk), app pages let you build a main page for your app, providing users with a dedicated space to interact with your app's features. You can also define additional pages for documentation, analytics, support, or any other functionality your app provides. Screenshot of an app's main page in HubSpot ## Key features * **Main page**: create a custom landing experience for users when they navigate to your app. * **Multiple pages**: build multiple pages within your app that users can navigate between. * **Layout components**: wrap groups of routes with shared UI such as navigation bars or sidebars. * **Page header actions**: add primary and secondary action buttons to the page header. * **Deep linking**: link directly to specific pages in your app. * **Wildcard routes**: match hierarchical paths for file browsers or documentation structures. * **Built with React**: use the same UI components and data fetching utilities available for app cards and settings pages. ## Start building out app pages * Follow the [quickstart guide](/docs/getting-started/quickstart) to start with a new app, or check out the [app creation guide](/docs/apps/developer-platform/build-apps/create-an-app) to customize the features and configuration of a new app. * Learn how to [create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages). * Define [routes for your pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing). * Link between [pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking). * Consult the [app pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference). ## Component reference documentation * [UI components overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview). * [PageHeader](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header). * [PageLink](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link). * [PageBreadcrumbs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-breadcrumbs). * [PageTitle](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-title). * [PageRoutes](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes). * [Link](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link). If you plan on distributing your app on the HubSpot App Marketplace, any app pages you've built are subject to a technical review from the HubSpot Ecosystem Quality team. # Page linking and navigation Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking Learn how to navigate between pages and pass data in app pages. Once you've defined routes in your app, you'll need to navigate between them. This guide covers the different methods for linking to pages and passing parameters when navigating. For information on how to access parameters in your page components, see the [accessing route information](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing#accessing-route-information) section in the routing guide. ## Navigation methods There are three primary ways to navigate between pages in your app. ### Using the PageLink component The `PageLink` component is the primary way to create clickable links to other pages within your app. ```tsx theme={null} import { PageLink } from "@hubspot/ui-extensions/pages"; // Link to home page Home // Link to a named page Documentation // Link with query parameters Getting Started ``` The `PageLink` component supports the following props: * `to` (string): The path to navigate to (e.g., `"/"`, `"/docs"`) * `params` (object): Query parameters to include in the URL For more details on the PageLink component, see the [PageLink component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link). The `Link` component from `@hubspot/ui-extensions` should only be used for external URLs. Use `PageLink` for navigating between pages within your app. ### Using PageHeader actions Page header actions support linking to app pages using `PageLink` components: ```tsx theme={null} import { Link } from "@hubspot/ui-extensions"; import { PageHeader } from "@hubspot/ui-extensions/pages"; Home Documentation Analytics Dashboard External Link ``` `PageHeader.PrimaryAction` and `PageHeader.SecondaryActions` can only contain `PageHeader.PageLink` (for internal pages) or `PageHeader.Link` (for external URLs) components. For more details on PageHeader components, see the [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header). ### Programmatic navigation You can navigate programmatically using the `navigateToPage` action from the extension context: ```tsx theme={null} import { Button, hubspot, useExtensionActions } from "@hubspot/ui-extensions"; const MyComponent = () => { const { navigateToPage } = useExtensionActions(); const handleClick = () => { navigateToPage({ to: '/docs', params: { section: 'api' } }); }; return ; }; ``` The `navigateToPage` function accepts an options object with: * `to` (string, required): The path to navigate to * `params` (object, optional): Query parameters to include ## Linking with path parameters When your routes include path parameters (e.g., `/view-contact/:contactId`), you can pass parameter values through the `params` object. The routing system will automatically substitute matching parameter names into the URL path. ### Basic path parameter linking ```tsx theme={null} import { PageLink } from "@hubspot/ui-extensions/pages"; // Route definition: // Link to a specific contact View Contact ``` **Generated URL:** `https://app.hubspot.com/app/{HubID}/{appId}/view-contact/123` The `contactId` parameter matches the `:contactId` path parameter, so it gets substituted into the path. ### Mixing path and query parameters When you provide multiple parameters, the routing system intelligently separates them: ```tsx theme={null} Contact Activity ``` **Generated URL:** `https://app.hubspot.com/app/{HubID}/{appId}/view-contact/123?tab=activity` ### Multiple path parameters You can link to routes with multiple path parameters by including all matching parameters: ```tsx theme={null} // Route definition: // View Note ``` **Generated URL:** `https://app.hubspot.com/app/{HubID}/{appId}/deals/456/notes/789?highlightSection=comments` ### How parameter substitution works The routing system follows these rules when generating URLs: 1. **Matching parameters go into the path**: If a parameter name matches a path parameter placeholder (e.g., `contactId` matches `:contactId`), it replaces that placeholder in the URL. 2. **Non-matching parameters become query parameters**: Any parameters that don't match path parameter names are added to the query string. 3. **Parameter order doesn't matter**: The system will correctly match parameters regardless of the order they appear in the `params` object. ### Programmatic navigation with path parameters The same substitution rules apply when using `navigateToPage`: ```tsx theme={null} const { navigateToPage } = useExtensionActions(); const viewContact = (contactId, initialTab) => { navigateToPage({ to: '/view-contact/:contactId', params: { contactId: contactId, // Goes into path tab: initialTab // Goes into query string } }); }; // Usage viewContact("12345", "overview"); // Navigates to: /view-contact/12345?tab=overview ``` For details on how to access parameters in your page components using `usePageRoute`, including parameter limitations and best practices, see the [Accessing route information](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing#accessing-route-information) section in the routing guide. ## See also * [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing) * [PageLink component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-link) * [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes) * [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header) * [Link component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/link) * [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference) # Page routing Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing Learn how to define routes and structure pages in app pages. App pages support routing, allowing you to create multiple pages within your app that users can navigate between. This guide covers how to define routes, including route types, path parameters, nested routes, and how paths are normalized. ## Route types The `PageRoutes` component supports three types of routes to handle different navigation scenarios. ### Index routes The index route is your app's home page, rendered when users navigate to the root URL of your app. Every app pages implementation should include an index route. ```tsx theme={null} import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages"; const PageRouter = createPageRouter( ); ``` **URL:** `https://app.hubspot.com/app/{HubID}/{appId}` The index route is always accessed with an empty path or `/`. You don't need to specify a path prop for index routes. ### Named routes Named routes define specific pages within your app. Each route has a unique path that users can navigate to. ```tsx theme={null} ``` Route paths are normalized automatically, so both `"docs"` and `"/docs"` will work. See [path normalization](#path-normalization) for details. **URLs:** * `https://app.hubspot.com/app/{HubID}/{appId}/docs` * `https://app.hubspot.com/app/{HubID}/{appId}/support` * `https://app.hubspot.com/app/{HubID}/{appId}/analytics` ### AnyRoute component The `AnyRoute` component defines a fallback route that renders when no other routes match. This is useful for displaying custom 404 pages or redirecting users. ```tsx theme={null} ``` The `AnyRoute` will render for any path that doesn't match defined routes, such as `/invalid-path` or `/does-not-exist`. It's recommended to always include an `AnyRoute` to provide a better user experience when users navigate to undefined paths. ### Wildcard routes (splat routes) Wildcard routes, also known as "splat" routes, allow you to match any characters following a specific path prefix. If a route path ends with `/*`, it will match any characters following the `/`, including other `/` characters. ```tsx theme={null} ``` **Example matches** The table below provides examples of route patterns and sample URLs that will and won't match: | Route Pattern | Matches | Does Not Match | | ------------- | --------------------------------------------------------------------------------------- | ------------------------------------------ | | `files/*` | `/files/documents/report.pdf`
`/files/images/photo.jpg`
`/files/a/b/c/d.txt` | `/file/doc.pdf`
`/documents/file.txt` | | `docs/*` | `/docs/getting-started`
`/docs/api/endpoints`
`/docs/guides/advanced/routing` | `/doc/guide`
`/documentation/api` | #### Accessing the wildcard value The matched portion of the URL is available as `params["*"]`: ```tsx theme={null} import { Heading, Text } from "@hubspot/ui-extensions"; import { usePageRoute, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages"; const FileBrowserPage = () => { const { params } = usePageRoute(); const filePath = params["*"]; // For URL: /files/documents/report.pdf // filePath = "documents/report.pdf" return ( <> Home Files {filePath} File Browser Current path: {filePath} ); }; ``` You can destructure the `*` parameter by assigning it a more descriptive name: ```tsx theme={null} const { params: { "*": filePath } } = usePageRoute(); // or const { params: { "*": splat } } = usePageRoute(); ``` #### Wildcard routes vs AnyRoute While both match multiple paths, they serve different purposes: **AnyRoute component:** * Catches any unmatched path at that nesting level * Typically used for 404 pages or fallback routes * Does not provide the matched path as a parameter **Wildcard routes (`path="prefix/*"`):** * Match a specific path pattern (e.g., `/docs/*` only matches paths starting with `/docs/`) * The matched portion is accessible as a parameter * Useful for file browsers, documentation sites, or any hierarchical content ```tsx theme={null} {/* Wildcard route matches /docs/anything */} {/* AnyRoute catches everything else that doesn't match */} ``` Use wildcard routes when you need to handle hierarchical paths (like file systems or documentation structures) and need access to the full path. Use AnyRoute for general 404 handling. ## Nested routes You can nest `PageRoutes` components within the route tree to create hierarchical page structures. Nest routes by adding a `` with a `path` prop as a child of the parent ``: ```tsx expandable theme={null} import React from "react"; import { Text, hubspot } from "@hubspot/ui-extensions"; import { createPageRouter, PageRoutes, PageLink, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages"; const HomePage = () => { return ( <> Home Home Content for the Home page... Visit Support ); }; const SupportIndexPage = () => { return ( <> Home Support Support Browse our support resources... Contact Us FAQ ); }; const ContactUsPage = () => { return ( <> Home Support Contact Us Contact Us Get in touch with our support team... ); }; const FAQPage = () => { return ( <> Home Support FAQ FAQ Find answers to common questions... ); }; const PageRouter = createPageRouter( ); hubspot.extend<"pages">(() => ); ``` This creates the following URL structure: * `/` - Home page * `/support` - Support index page * `/support/contact-us` - Contact us page * `/support/faq` - FAQ page ### Nested route behavior When using nested routes: * A `` with a `path` prop defines a group of nested routes under that path * The nested `IndexRoute` renders when the path matches exactly the parent path (`/support`) * Named routes within the nested `PageRoutes` extend the parent path (`/support/contact-us`) * Each nested level can have its own `AnyRoute` for unmatched paths within that section ### Nesting with path parameters You can combine nested routes with path parameters to create hierarchical structures: ```tsx theme={null} const PageRouter = createPageRouter( ); ``` This creates routes like: * `/deals/123` - Deal overview * `/deals/123/notes` - Deal notes list * `/deals/123/notes/456` - Specific note details ## Layout components Layout components let you wrap groups of routes with shared UI, such as navigation bars, sidebars, or other persistent elements. A layout component receives `children` as a prop and renders the matched route's content within it. ### Basic layout Pass a layout component to `` using the `layoutComponent` prop: ```tsx theme={null} import { ReactNode } from "react"; import { Text, Flex } from "@hubspot/ui-extensions"; import { createPageRouter, PageRoutes, PageLink, usePageRoute } from "@hubspot/ui-extensions/pages"; function AppLayout({ children }: { children: ReactNode }) { const { path } = usePageRoute(); return ( Home Docs Support {children} ); } const PageRouter = createPageRouter( ); ``` The `AppLayout` component will wrap every page rendered by these routes. The matched page component is rendered in place of `{children}`. ### Nested layouts You can apply different layout components at different nesting levels. Nested `` can define their own `layoutComponent`, which will render inside the parent layout: ```tsx theme={null} function CustomersLayout({ children }: { children: ReactNode }) { return ( Customers {children} ); } const PageRouter = createPageRouter( ); ``` When a user navigates to `/customers`, the rendered output will be `AppLayout` > `CustomersLayout` > `ListCustomersPage`. Each layout wraps the content below it in the route tree. ## Path parameters Path parameters allow you to create dynamic routes that can capture values from the URL path. This is useful for pages that display specific items, such as viewing a particular contact, deal, or custom object record. ### Defining routes with path parameters To define a route with path parameters, use a colon (`:`) followed by the parameter name in the route path: ```tsx theme={null} ``` You can include multiple path parameters in a single route, as shown in the `documents/:folderId/:documentId` example. ### Understanding path parameters Path parameters are placeholders in your route definitions that capture values from the URL. When a user navigates to a URL that matches the route pattern, the parameter values are extracted and made available to your component. **Example route pattern:** `/view-contact/:contactId` This pattern will match URLs like: * `/view-contact/123` (contactId = "123") * `/view-contact/abc-def` (contactId = "abc-def") * `/view-contact/contact-456` (contactId = "contact-456") But will not match: * `/view-contact` (missing contactId) * `/view-contact/123/edit` (additional path segment) ### Multiple path parameters When using multiple path parameters, each segment of the URL path can capture a different value: ```tsx theme={null} // Route definition ``` This route pattern creates a hierarchical structure where: * `/deals/456/notes/789` matches with `dealId="456"` and `noteId="789"` * The path segments between parameters (`deals` and `notes`) must match exactly * Both parameters are required for the route to match ### Best practices for path parameters * **Use descriptive parameter names**: Choose names that clearly indicate what the parameter represents (e.g., `:contactId`, `:dealId`, not `:id`) * **Keep parameter counts reasonable**: While you can have multiple path parameters, too many can make URLs hard to read * **Validate parameter values**: Always validate path parameter values before using them (e.g., check if an ID exists) * **Use path parameters for resource IDs**: Path parameters work well for resource identifiers that define what page you're viewing * **Use query parameters for optional state**: Use query parameters for filtering, sorting, or view state that doesn't change what resource you're viewing * **Use wildcards for hierarchical paths**: When you need to match multiple nested segments and preserve the full path (e.g., file systems, documentation), use wildcard routes instead of multiple path parameters ## Accessing route information Use the `usePageRoute` hook to access the current page path, route ID, and all parameters (both path parameters and query parameters) in your page components. The hook returns an object with `path`, `routeId`, and `params`. ### Basic usage ```tsx theme={null} import { Text } from "@hubspot/ui-extensions"; import { usePageRoute, PageBreadcrumbs, PageTitle } from "@hubspot/ui-extensions/pages"; const DocsPage = () => { const { path, params } = usePageRoute(); const { section, topic } = params; return ( <> Home Documentation Documentation Current path: {path} {section && Section: {section}} {topic && Topic: {topic}} ); }; ``` When a user visits `https://app.hubspot.com/app/{HubID}/{appId}/docs?section=api&topic=authentication`, the hook will return: * `path`: `"/docs"` * `params.section`: `"api"` * `params.topic`: `"authentication"` ### Accessing path parameters Path parameters are included in `params` alongside query parameters - the `usePageRoute` hook returns all parameters regardless of their source: ```tsx theme={null} // Route definition: const ContactDetailsPage = () => { const { params } = usePageRoute(); const { contactId, tab } = params; // contactId comes from the path: /view-contact/123 // tab comes from query string: ?tab=activity return ( <> Home Contacts Contact {contactId} Contact {contactId} Contact ID: {contactId} {tab && Active tab: {tab}} ); }; ``` When a user visits `/view-contact/123?tab=activity`: * `contactId`: `"123"` (from path) * `tab`: `"activity"` (from query string) ### Accessing wildcard parameters Wildcard values are accessed through the `params["*"]` property: ```tsx theme={null} import { usePageRoute } from "@hubspot/ui-extensions/pages"; const FileBrowserPage = () => { const { params } = usePageRoute(); const wildcardValue = params["*"]; // For URL: /files/documents/2024/report.pdf // wildcardValue = "documents/2024/report.pdf" return ( <> File Browser Path: {wildcardValue} ); }; ``` You can destructure the `*` parameter by assigning it a more descriptive name: ```tsx theme={null} const { params: { "*": filePath } } = usePageRoute(); // or const { params: { "*": splat } } = usePageRoute(); ``` When a user visits `/files/documents/2024/report.pdf` with a route defined as `path="/files/*"`: * `params["*"]`: `"documents/2024/report.pdf"` Like all page parameters, wildcard values are strings. Remember to validate and sanitize the wildcard value before using it, especially if using it to access files or resources. ## Route IDs Each route can be assigned a stable `id` prop that uniquely identifies it. When a route matches, its `id` is available as `routeId` from the `usePageRoute` hook. This is particularly useful in layout components where you need to determine which route is currently active without relying on path matching. Route IDs must be unique within your collection of routes. Having multiple routes with the same `id` will result in an error. ### Defining route IDs Add an `id` prop to any route component: ```tsx theme={null} const PageRouter = createPageRouter( ); ``` ### Accessing the route ID Use the `routeId` property from the `usePageRoute` hook: ```tsx theme={null} import { usePageRoute } from "@hubspot/ui-extensions/pages"; const MyComponent = () => { const { routeId } = usePageRoute(); // routeId is "home", "support", "not-found", etc. }; ``` ### Using route IDs in layout components Route IDs are especially useful in layout components where you need to render different breadcrumbs, titles, or other shared UI based on the active route. Unlike path-based matching, route IDs remain stable even if route paths change, and they work reliably with dynamic path parameters. ```tsx theme={null} import { ReactNode } from "react"; import { Text, Flex } from "@hubspot/ui-extensions"; import { createPageRouter, PageRoutes, PageLink, PageBreadcrumbs, PageTitle, usePageRoute } from "@hubspot/ui-extensions/pages"; const BREADCRUMBS: Record = { home: Home, support: ( <> Home Support ), "not-found": ( <> Home Not Found ), }; const TITLES: Record = { home: "Home", support: "Support", "not-found": "Page Not Found", }; function AppLayout({ children }: { children: ReactNode }) { const { routeId } = usePageRoute(); return ( {BREADCRUMBS[routeId]} {TITLES[routeId]} {children} ); } const PageRouter = createPageRouter( ); ``` Route IDs provide a more robust way to identify the active route compared to path matching. They don't break when paths are renamed and work naturally with routes that contain dynamic path parameters. ## Parameter limitations All page parameters are treated as strings. If you pass numbers, booleans, or other types, they will be converted to strings. Understanding parameter limitations helps you avoid common issues when working with app pages. ### All values are strings Parameters are always received as strings, regardless of the type you pass. ```tsx theme={null} // Passing parameters navigateToPage({ to: '/analytics', params: { count: 42, // Will be "42" enabled: true, // Will be "true" userId: "12345" // Will be "12345" } }); // Receiving parameters const { params } = usePageRoute(); const { count, enabled, userId } = params; // count is "42" (string, not number) // enabled is "true" (string, not boolean) // userId is "12345" (string) // You'll need to convert them if needed const countNumber = parseInt(count, 10); const enabledBoolean = enabled === "true"; ``` Always validate and convert parameter values before using them. Check for `null`, `undefined`, or invalid formats to handle cases where parameters are missing or malformed. ### No complex objects You cannot pass objects, arrays, or other complex data structures as parameters. Only simple string values are supported. ```tsx theme={null} // ❌ NOT supported navigateToPage({ to: '/page', params: { user: { name: "John", age: 30 }, // Objects not supported items: [1, 2, 3] // Arrays not supported } }); // ✅ Supported - use JSON.stringify and parse if needed navigateToPage({ to: '/page', params: { user: JSON.stringify({ name: "John", age: 30 }) } }); // Then parse it when accessing const { params } = usePageRoute(); const userData = params.user ? JSON.parse(params.user) : null; ``` When using JSON.stringify for complex data, be mindful of URL length limits. Very large objects will create excessively long URLs. ### URL length limits Query parameters are part of the URL, which has practical length limits: * **Browsers**: Most browsers support URLs up to 2,000-8,000 characters * **Best practice**: Keep URLs under 2,000 characters for maximum compatibility * **Recommendation**: Use parameters for IDs and simple state, not for large data payloads If you need to pass large amounts of data: 1. Store the data on your backend 2. Pass only an ID through a URL parameter 3. Fetch the full data using the ID when the page loads ```tsx theme={null} // ❌ Bad - passing large data through URL navigateToPage({ to: '/report', params: { data: JSON.stringify(hugeDataObject) // Too large! } }); // ✅ Good - pass ID and fetch data navigateToPage({ to: '/report', params: { reportId: "report-123" // Small ID } }); // In the report page, fetch the full data const ReportPage = () => { const { params } = usePageRoute(); // Fetch report data using the ID const reportData = useFetchReport(params.reportId); // ... }; ``` ## Path normalization When navigating to pages or defining routes, the routing system automatically normalizes paths to ensure consistent behavior. Understanding these normalization rules can help you avoid unexpected routing issues. ### Normalization rules The following transformations are applied to paths: 1. **Leading slashes are added**: If a path doesn't start with `/`, one is automatically added. * `"foo"` → `"/foo"` * `"docs"` → `"/docs"` 2. **Trailing slashes are removed**: Paths should not end with a slash, and any trailing slashes are automatically removed. * `"foo/"` → `"/foo"` * `"docs/"` → `"/docs"` * `"support/contact/"` → `"/support/contact"` 3. **Multiple consecutive slashes are collapsed**: Any sequence of multiple slashes is reduced to a single slash. * `"foo//bar"` → `"/foo/bar"` * `"docs///api"` → `"/docs/api"` * `"//support"` → `"/support"` 4. **Empty paths are treated as root**: An empty string or single slash both represent the home page. * `""` → `"/"` * `"/"` → `"/"` ### Examples Here are some examples of how different path formats are normalized: | Input Path | Normalized Path | | ------------------ | ---------------- | | `"docs"` | `"/docs"` | | `"/docs"` | `"/docs"` | | `"docs/"` | `"/docs"` | | `"/docs/"` | `"/docs"` | | `"support/faq"` | `"/support/faq"` | | `"support//faq"` | `"/support/faq"` | | `"//docs///api//"` | `"/docs/api"` | | `""` | `"/"` | | `"/"` | `"/"` | ### Practical implications Because of path normalization: * **You can use either format in route definitions**: Both `path="docs"` and `path="/docs"` will work the same way after normalization. ```tsx theme={null} // Both are equivalent after normalization ``` * **Navigation calls are forgiving**: When navigating to pages, you don't need to worry about exact path formatting. See the [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking) guide for more details. * **Consistency in comparisons**: When comparing paths using `usePageRoute()`, always compare against normalized paths. ```tsx theme={null} import { usePageRoute } from "@hubspot/ui-extensions/pages"; const MyComponent = () => { const { path } = usePageRoute(); // path is always normalized, so compare with normalized format if (path === "/docs") { // This will match /docs, docs/, /docs/, etc. } return Current page: {path}; }; ``` While path normalization is forgiving, it's best practice to use consistent path formatting throughout your code. ## Getting the current page path Use the `usePageRoute` hook to get the current page path. This is useful for determining which page is currently active, which can help with navigation highlighting and conditional rendering. ```tsx theme={null} import { usePageRoute, PageLink } from "@hubspot/ui-extensions/pages"; const Navigation = () => { const { path } = usePageRoute(); // For home page: "/" // For /docs: "/docs" // For /support/faq: "/support/faq" return ( Home Docs Support ); }; ``` The `path` value returned by `usePageRoute` is always the normalized path, so you can reliably compare it against expected values. ### Common use cases **Highlighting active navigation items:** ```tsx theme={null} const { path } = usePageRoute(); const isActive = path === "/docs"; ``` **Identifying the active route in layout components:** ```tsx theme={null} const { routeId } = usePageRoute(); const showSidebar = routeId === "dashboard" || routeId === "analytics"; ``` **Conditional rendering based on current page:** ```tsx theme={null} const { path } = usePageRoute(); const showBreadcrumbs = path.includes("/support/"); ``` **Analytics and tracking:** ```tsx theme={null} const { path } = usePageRoute(); useEffect(() => { trackPageView(path); }, [path]); ``` ## Related resources * [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking) * [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes) * [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header) * [Create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages) * [App pages reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference) # App pages reference Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/reference Reference information for building app pages on the latest version of the developer platform. Below, find reference information for building app pages. ## Project structure To add app pages to an app, create a `pages` directory within `src/app`. The `pages` directory should contain: * A JSON configuration file that defines the pages configuration (`Pages-hsmeta.json` is recommended). * A React file that renders the pages component (`Pages.jsx` or `Pages.tsx` is recommended). * A `package.json` file to handle any needed dependencies. ```shell theme={null} project-folder/ └── src/ └── app/ ├── app-hsmeta.json └── pages/ ├── pages-hsmeta.json ├── Pages.tsx └── package.json ``` ## App pages configuration In the `*-hsmeta.json` configuration file for your app pages, include the properties below. ```json theme={null} { "uid": "my-app-pages", "type": "page", "config": { "entrypoint": "/app/pages/Pages.tsx" } } ``` | Field | Type | Description | | --------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | | `uid` | String | The pages component's unique identifier. This can be any string, but should meaningfully identify the component. | | `type` | String | The type of component, which should be `page` in this case. | | `config` | Object | An object containing configuration details. | | `entrypoint` | String | The file path of the pages component's front-end React code. | ## Building the React front-end The UI of app pages is created by a React component file, either `.jsx` or `.tsx`. This file lives in the `pages/` directory alongside the [pages configuration JSON file](#app-pages-configuration) (`*-hsmeta.json`). In the configuration file, you'll specify the path of the React file in the `entrypoint` field. Routes are defined using `createPageRouter`, which accepts JSX route definitions and returns a React component. For complete examples, see the [create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages) guide. ## App page URLs Your app pages can be accessed using the following URL patterns: | Target | URL | | ---------------------- | ------------------------------------------------------------------------------- | | Home page | `https://app.hubspot.com/app/{hubId}/{appId}` | | Home page with params | `https://app.hubspot.com/app/{hubId}/{appId}?param1Name=param1Value` | | Named page | `https://app.hubspot.com/app/{hubId}/{appId}/{pagePath}` | | Named page with params | `https://app.hubspot.com/app/{hubId}/{appId}/{pagePath}?param1Name=param1Value` | For example, following the URL path structure above, if a user who installed your app has a HubSpot account ID (i.e., their `hubId`) of 12345 and your app ID was 67890, they could navigate to their home page at the following URL: ``` https://app.hubspot.com/app/12345/67890 ``` ## React page hooks App pages provide hooks to access routing information within your page components. ### usePageRoute Use the `usePageRoute` hook to access the current page path and all parameters (both path parameters and query parameters): ```tsx theme={null} import { usePageRoute } from "@hubspot/ui-extensions/pages"; const MyPage = () => { const { path, routeId, params } = usePageRoute(); // path will be "/" for the home page or the current page path like "/docs" // routeId is the id assigned to the matched route (e.g., "home", "docs") // params contains both path parameters and query parameters return ( <> Current path: {path} Route: {routeId} Params: {params.foo}, {params.anotherParam} ); }; ``` The `usePageRoute` hook returns an object with: | Property | Type | Description | | --------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- | | `path` | String | The current normalized page path (e.g., `"/"`, `"/docs"`). | | `routeId` | String | The `id` of the matched route, as defined on the route's `id` prop. Useful for identifying the active route in layout components. | | `params` | Object | An object containing all parameters, including both path parameters (e.g., `:contactId`) and query parameters from the URL. | ## Programmatic navigation You can navigate to app pages programmatically using the `navigateToPage` action from the `useExtensionActions` hook: ```tsx theme={null} import { Button, hubspot, useExtensionActions } from "@hubspot/ui-extensions"; const MyComponent = () => { const { navigateToPage } = useExtensionActions(); const handleNavigate = () => { navigateToPage({ to: '/docs', params: { foo: 'bar', anotherParam: 'abc' } }); }; return ( ); }; ``` The `navigateToPage` function accepts an options object with the following properties: | Property | Type | Description | | -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | `to` | String | The path to navigate to (e.g., `"/"` for home, `"/docs"` for a named page). The path will be normalized (e.g., `"docs"` becomes `"/docs"`). | | `params` | Object | An object containing query parameters to include in the URL (e.g., `{ foo: 'bar' }`). | ## createPageRouter The `createPageRouter` function accepts JSX route definitions and returns a React component that handles routing. Call it at the module level with your `` tree: ```tsx theme={null} import { createPageRouter, PageRoutes } from "@hubspot/ui-extensions/pages"; const PageRouter = createPageRouter( ); function App() { return ; } ``` ### Parameters | Parameter | Type | Description | | ------------------------------ | ----------- | ----------------------------------------------------------- | | `routes` | `ReactNode` | A `` JSX tree defining all routes for your app. | ### Returns A React component (`PageRouter`) that you render in your app to activate routing. ## Page routing components The `PageRoutes` component and its sub-components are used with [`createPageRouter`](#createpagerouter) to define routing for app pages: * **`PageRoutes`**: Container for route definitions. Supports `layoutComponent` for wrapping child routes with shared UI. * **`PageRoutes.IndexRoute`**: Defines the home page (index route) that renders at the root URL of your app. * **`PageRoutes.Route`**: Defines a named page route with a `path` and `component`. * **`PageRoutes.AnyRoute`**: Defines a catch-all route for unmatched paths, useful for custom 404 pages. Each route sub-component also accepts an optional `id` prop. When a route matches, the `id` value is available as `routeId` from `usePageRoute()`. See the [route IDs](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing#route-ids) section in the routing guide for details. For detailed documentation on routing components including nested routes, path parameters, and wildcard routes, see: * [Page routing guide](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing) * [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes) ## Manage dependencies You can include dependencies for your app pages in a `package.json` file within the `pages/` directory. By default, when adding app pages through the `hs project add` command, a `package.json` file will be created for you with the following dependencies: * `@hubspot/ui-extensions` * `react` * `typescript` To install dependencies for project components with a `package.json` file, you can run the `hs project install-deps` command in your project directory. ```json theme={null} { "name": "hubspot-example-extension", "version": "0.1.0", "license": "MIT", "dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" }, "devDependencies": { "typescript": "^5.3.3" } } ``` ## Related resources * [Create app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages) * [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing) * [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking) * [Testing app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/testing) * [PageHeader component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-header) * [PageRoutes component reference](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/app-page-components/page-routes) # Testing app pages Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/testing Learn how to test app pages with mocked routing, paths, and parameters. App pages support routing and navigation, which requires special consideration when writing tests. This guide shows you how to test app pages using the UI extensions SDK testing utilities with mocked routing state, paths, and parameters. ## Create mocks When you create a renderer with `createRenderer('pages')`, the following mocks are available: ```tsx theme={null} const { mocks, render, findByTestId } = createRenderer('pages'); // Mock the current route (controls usePageRoute) mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123', tab: 'activity' } }); // Mock navigation action mocks.actions.navigateToPage; // FunctionSpy for testing navigation calls ``` Keep the following in mind as you mock out your components and their behavior: * Use `'pages'` as the extension point location when creating a renderer * Mock the current route using `mocks.router.setCurrentRoute()` with path and params * Always render the full `` component with ``, not individual page components * Use `findByTestId` for reliable component querying in tests ## Testing app pages with routing and parameters This example demonstrates the most common testing scenario: rendering an app with routing and testing that the correct page component displays with the right parameters. ```tsx theme={null} import { createRenderer } from '@hubspot/ui-extensions/testing'; import { Text, Alert } from '@hubspot/ui-extensions'; import { createPageRouter, PageRoutes, usePageRoute } from '@hubspot/ui-extensions/pages'; // Define page components const HomePage = () => Home Page; const ContactDetailsPage = () => { const { path, params } = usePageRoute(); const { contactId, tab } = params; if (!contactId) { return Missing contact ID; } return ( <> Contact ID: {contactId} Tab: {tab || 'overview'} Path: {path} ); }; const NotFoundPage = () => Page Not Found; // Define the app structure with routes const PageRouter = createPageRouter( ); const AppPages = () => ; // Test: home page renders test('renders home page for root path', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/', params: {} }); render(); expect(findByTestId(Text, 'home-page')).toBeDefined(); }); // Test: page with path parameter test('renders contact page with path parameter', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123' } }); render(); expect(findByTestId(Text, 'contact-id').text).toEqual('Contact ID: 123'); expect(findByTestId(Text, 'current-path').text).toEqual('Path: /contacts/:contactId'); }); // Test: page with path and query parameters test('renders contact page with path and query parameters', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '456', tab: 'activity' } }); render(); expect(findByTestId(Text, 'contact-id').text).toEqual('Contact ID: 456'); expect(findByTestId(Text, 'active-tab').text).toEqual('Tab: activity'); }); // Test: error state when parameter is missing test('shows error when contact ID is missing', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '' } }); render(); expect(findByTestId(Alert, 'error-alert')).toBeDefined(); }); // Test: 404 page for unknown route test('renders 404 page for unknown path', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/unknown-path', params: {} }); render(); expect(findByTestId(Text, '404-page')).toBeDefined(); }); ``` Always render the full `` component with `` in your tests. This ensures you're testing the actual routing behavior, not just individual page components in isolation. ## Testing navigation The following code blocks demonstrate how to test that your components call `navigateToPage` with the correct arguments: ```tsx theme={null} import { Button } from '@hubspot/ui-extensions'; const GoToContactButton = ({ context }) => { const handleClick = () => { context.actions.navigateToPage({ pagePath: '/contacts/:contactId', params: { contactId: '123', tab: 'activity' } }); }; return ( ); }; test('navigates when button is clicked', () => { const { mocks, render, findByTestId } = createRenderer('pages'); render(); // Trigger navigation findByTestId(Button, 'nav-button').trigger('onClick'); // Assert navigation was called expect(mocks.actions.navigateToPage.called).toBe(true); expect(mocks.actions.navigateToPage.callCount).toBe(1); // Assert navigation arguments const [navArgs] = mocks.actions.navigateToPage.calls[0]; expect(navArgs).toEqual({ pagePath: '/contacts/:contactId', params: { contactId: '123', tab: 'activity' } }); }); ``` ## Testing route IDs When your components use `routeId` from `usePageRoute()`, include the `routeId` field in your `setCurrentRoute` call to mock the matched route's ID: ```tsx theme={null} import { createRenderer } from '@hubspot/ui-extensions/testing'; import { Text } from '@hubspot/ui-extensions'; import { createPageRouter, PageRoutes, PageBreadcrumbs, PageTitle, PageLink, usePageRoute } from '@hubspot/ui-extensions/pages'; function AppLayout({ children }: { children: ReactNode }) { const { routeId } = usePageRoute(); const titles: Record = { home: 'Home', support: 'Support', }; return ( <> {titles[routeId]} {children} ); } const PageRouter = createPageRouter( ); const AppPages = () => ; test('layout renders correct title based on routeId', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/support', routeId: 'support', params: {} }); render(); expect(findByTestId(PageTitle, 'page-title').children).toEqual('Support'); }); ``` ## Testing recommendations and examples The sections below provide guidance on how to test your app pages and their associated functionality: ### Always render the full app structure Render `` with ``, not individual page components: ```tsx theme={null} // Good - tests actual routing const PageRouter = createPageRouter( ); const AppPages = () => ; test('renders contact page', () => { const { mocks, render } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123' } }); render(); // ... }); // Bad - bypasses routing test('renders contact page', () => { const { mocks, render } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123' } }); render(); // Doesn't test routing! // ... }); ``` ### Use findByTestId for reliable querying Add `testId` props to components for stable, maintainable tests: ```tsx theme={null} const ContactPage = () => { const { params } = usePageRoute(); return ( <> Contact: {params.contactId} Active ); }; test('displays contact', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123' } }); render(); expect(findByTestId(Text, 'contact-id').text).toEqual('Contact: 123'); }); ``` ### Mock only what you need Only mock the parameters your component actually uses: ```tsx theme={null} // Good - minimal mocking test('displays contact name', () => { const { mocks, render } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123' } }); render(); // ... }); // Unnecessary - extra params test('displays contact name', () => { const { mocks, render } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts/:contactId', params: { contactId: '123', dealId: '456', // Unused companyId: '789' // Unused } }); render(); // ... }); ``` ### Test error states Test how your pages handle missing or invalid parameters: ```tsx theme={null} const ContactPage = () => { const { params } = usePageRoute(); if (!params.contactId) { return Missing contact ID; } return Contact: {params.contactId}; }; test('shows error when contact ID is missing', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/contacts', params: {} }); render(); expect(findByTestId(Alert, 'error')).toBeDefined(); }); ``` ### Remember parameters are strings All parameters are strings. Convert types when needed: ```tsx theme={null} const AnalyticsPage = () => { const { params } = usePageRoute(); // Convert string params to proper types const countNumber = parseInt(params.count || '0', 10); const enabledBoolean = params.enabled === 'true'; return ( <> Count: {countNumber} Enabled: {enabledBoolean ? 'Yes' : 'No'} ); }; test('converts parameter types', () => { const { mocks, render, findByTestId } = createRenderer('pages'); mocks.router.setCurrentRoute({ path: '/analytics', params: { count: '42', enabled: 'true' } }); render(); expect(findByTestId(Text, 'count').text).toEqual('Count: 42'); expect(findByTestId(Text, 'enabled').text).toEqual('Enabled: Yes'); }); ``` ## Related resources Check out the following guides for additional guidance on testing and navigating between your pages: * [Testing overview](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/overview) * [Testing reference](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/reference) * [Mocking](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/mocking) * [Page routing](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-routing) * [Page linking and navigation](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/page-linking) # Create a settings page Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page Learn how to create a settings page for your app. You can create a React-based settings page for your app on the latest versions (`2025.2` and `2026.03`) of the developer platform, that users who install your app can navigate to and customize for their account. This guide walks you through how to build a settings component, using React, which replaces the [previous settings experience](/docs/apps/legacy-apps/public-apps/create-an-app-settings-page). If you deploy your new settings component, users who install your app going forward will immediately see your new settings extension instead of any previously built settings page. Before you begin, follow the [quickstart guide](/docs/apps/developer-platform/add-features/app-objects/quickstart-guide-to-app-objects) to create your first app on the latest version of the developer platform. **Please note:** if you previously created a [settings page](/docs/apps/legacy-apps/public-apps/create-an-app-settings-page) for your legacy app, you'll need to refactor your settings experience using the new configuration options outlined in this guide. It's recommended that you note all of the interface elements in the current production version of your app before you start configuring your new settings component on the latest version of the developer platform. Once you move to the new settings component for your app, you'll lose access to the previous WYSIWYG configuration UI you previously used. ## Prerequisites * Ensure you've installed the latest beta version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). * If you haven't done so yet, [create a new app](/docs/getting-started/quickstart). ## Create an app settings page To add a settings page component to an existing app, use the terminal to navigate into your local project directory, then run the following command: ```shell theme={null} hs project add ``` Then, when prompted to select a component to add, select **Settings**. ![Screenshot of the terminal prompt for selecting the type of component to add](https://developers.hubspot.com/hubfs/Knowledge_Base_2023-24-25/developer/add-component-settings-page.png) A new `settings/` directory will be created in the project's `src/app/` directory. The `settings/` directory will contain: * A JSON configuration file (`*-hsmeta.json`) * A React component file (`.jsx`) * A `package.json` file ```shell theme={null} myProject └── src/ └── app/ └── settings/ ├── new-settings-page-hsmeta.json ├── NewSettingsPage.tsx └── package.json ``` ```json theme={null} { "uid": "settings_extension", "type": "settings", "config": { "entrypoint": "/app/settings/NewSettingsPage.tsx" } } ``` ```tsx theme={null} import React from "react"; import { EmptyState, Text } from "@hubspot/ui-extensions"; import { hubspot } from "@hubspot/ui-extensions"; hubspot.extend(({ context }) => { return ; }); const NewSettingsPage = ({ context }) => { return ( Build your application settings page here! ); }; ``` ```json theme={null} { "name": "hubspot-example-extension", "version": "0.1.0", "license": "MIT", "dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" }, "devDependencies": { "typescript": "^5.3.3" } } ``` To upload your settings page to HubSpot: * Run `hs project install-deps` from within your local project directory to install necessary dependencies. This will create a `package-lock.json` file, which will speed up the build of the uploaded settings extension, as well as ensure that any dependencies in your local development environment and production match. * Then, run `hs project upload`. * After the project finishes deploying, open the project in HubSpot by running `hs project open`. Alternatively, in HubSpot you can navigate to **Development** > **Projects**, then click the **name** of your project. * Your settings component should now be listed on the details page. Uploaded settings component on project details page ## View the app settings page in HubSpot To verify the settings component is working correctly: * In the HubSpot account where you've installed the app, click the **Marketplace** icon, then click **Connected apps**. Navigate to connected apps in developer test account * Click the **My apps** tab to view a list of the account's currently installed apps. * Click the **name** of your app, which will redirect you to your app's overview page. * On the overview page, click the **Settings** tab. Screenshot of an app's settings page in HubSpot. You can now continue to build out your app's settings page as needed. Similar to building app cards, you'll use the UI extensions SDK to add functionality and visual elements to your settings page. Note that all the existing limitations around building UI extensions apply to building a settings page. * Use `hubspot.fetch` to leverage your backend to save and retrieve settings. Learn more about using this approach in the legacy documentation. * Check out the reference documentation on [standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for how to use React components when building your extension, or use the component in the [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit). * Use the `hs project dev` command to iteratively build out your settings page and preview your changes locally. The local development server will only pick up changes saved to the front-end React file. If you update the `*-hsmeta.json` or `package.json` files, you'll need to stop the server, upload your changes, then start the server again. ## Component best practices The sections below outline several best practices to keep in mind as you build out the settings experience for your app. ### Organizing content If you have enough content in your settings extension to warrant the need to separate and organize all of the user's settings data, you should consider using the [Panel](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/panel), [Modal](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal), [Accordion](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/accordion), and [Tabs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tabs) components accordingly. Consider how you want to present and arrange your settings and the corresponding data that should be fetched for each component. ### Tabs If you're using [Tabs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/tabs), use the `default` tab variant. The settings extension is already contained within an enclosed variant tab, and a second layer of enclosed tabs will visually clash with the design. Example of nested tabs on settings page The following snippet outlines how to structure your tabs. ```jsx theme={null} Here is the content of the first tab. This is where the content of the second tab goes. ; ``` Refer to [individual component reference docs](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) for more best practices, guidelines, and code examples to get you started. HubSpot also provides design pattern guidance for [Button](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/buttons), [Form](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/forms), and [Table](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/patterns/tables) component usage. # Fetching data for UI extensions Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/fetching-data Learn the different ways to fetch data in UI extensions, including CRM hooks, actions, serverless functions, and the hubspot.fetch() API. UI extensions have multiple ways to fetch data. The right approach depends on what data you need and where the extension runs. | Method | When to use | Where it works | | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | | [`useCrmProperties`](#crm-data-hooks) | Fetch properties from the current CRM record with automatic state management, formatting, and live updates. | CRM extension points only | | [`useAssociations`](#crm-data-hooks) | Fetch records associated with the current CRM record with pagination and formatting. | CRM extension points only | | [`actions.fetchCrmObjectProperties`](#fetchcrmobjectproperties-action) | Fetch CRM property data with manual control over when fetching occurs (imperative) | CRM extension points only | | [`hubspot.serverless()`](#serverless-functions) | Retrieve or write data server-side using JavaScript within HubSpot's infrastructure, removing the need to manage your own server. | All extension points | | [`hubspot.fetch()`](#hubspot-fetch) | Send requests from your extension to a backend that you manage, or to a public endpoint. | All extension points | If you receive raw property values via `actions.fetchCrmObjectProperties`, a `hubspot.serverless()` call that reads CRM data, or a `hubspot.fetch()` request to the HubSpot CRM APIs, you can pass the results to the [`formatCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/utilities) utility to apply HubSpot's display formatting. For example, resolve enumeration values to their display labels. ## CRM data hooks The CRM data hooks fetch data using the context of the CRM record where an extension is displayed. They handle loading state, error handling, and property formatting automatically. They also offer utility functions for data refetching and pagination. The CRM-specific hooks are only available in CRM extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, and `helpdesk.sidebar`. Learn more about using CRM data hooks on the [hooks reference page](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#CRM-specific-hooks). ```jsx theme={null} import { useCrmProperties, useAssociations } from "@hubspot/ui-extensions/crm"; ``` ## fetchCrmObjectProperties action Use `actions.fetchCrmObjectProperties` to fetch property values from the current CRM record imperatively. For example, when you need to control exactly when fetching occurs rather than fetching on render. This action is only available in CRM extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, and `helpdesk.sidebar`. For most use cases, [`useCrmProperties`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks#usecrmproperties) is the better choice. It provides automatic state management, property formatting, and live updates when properties change. Learn more about using `fetchCrmObjectProperties` on the [actions reference page](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions#fetch-crm-property-data). ## Serverless functions Use `hubspot.serverless()` to call a serverless function from your UI extension. Serverless JavaScript functions execute within HubSpot's infrastructure, which eliminates the need to manage your own external server. Learn more about [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview). **Please note:** an *Enterprise* subscription is required to install an app with serverless functions. You can use a [developer test account](/docs/getting-started/account-types#developer-test-accounts) to test serverless functionality during development without an *Enterprise* subscription. ```jsx theme={null} import { hubspot } from '@hubspot/ui-extensions'; const result = await hubspot.serverless('my_function', { parameters: { key: 'value' }, propertiesToSend: ['firstname', 'email'] }); ``` ## hubspot.fetch() Use `hubspot.fetch()` to make requests from a UI extension to your own backend or a third-party API. It works in all extension points (app cards, app homes, app settings, etc.). To fetch data using this method, you'll need to: * Provide a REST-based backend service to handle requests. * Include the request URLs as `permittedUrls` in the [app's configuration](/docs/apps/developer-platform/build-apps/app-configuration). **Please note:** apps that use [Sensitive Data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) cannot use `hubspot.fetch()`, preventing Sensitive Data from being sent to external services. ### Specify permitted URLs to fetch from To make calls to your backend or a third-party service, update the app's `*-hsmeta.json` [configuration file](/docs/apps/developer-platform/build-apps/app-configuration) to include the URLs you'll be requesting. The URLs must be specified in the `fetch` array of `permittedUrls`. ```json theme={null} "permittedUrls": { "fetch": ["https://api.example.com/api/data/"], "img": [], "iframe": [] } ``` Note the following when configuring permitted URLs: * Fetch URLs must be valid HTTPS URLs and cannot be `localhost`. If you want to send requests to a locally running backend, learn how to [proxy requests](#proxying-requests-to-a-locally-running-backend). * Entries in `permittedUrls.fetch` are treated as prefixes. A request to `https://api.example.com/api/data/users` will be allowed if `https://api.example.com/api/data/` is listed. * Wildcards are not supported. * If a URL is not included in `permittedUrls.fetch`, the request will fail with a 403 Forbidden error. | `permittedUrls` entry | Request URL | Allowed? | | ----------------------------------- | ---------------------------------------- | -------- | | `https://api.example.com/api/data/` | `https://api.example.com/api/data/users` | Yes | | `https://api.example.com/api/data/` | `https://api.example.com/other/endpoint` | No | | `https://api.example.com/` | `https://api.example.com/api/data/users` | Yes | When running the local development server, any `hubspot.fetch()` request will still go to your hosted backend via a HubSpot-managed data fetch service. If you need to update the allowlist while running the dev server, you'll need to run `hs project upload` for the change to take effect. ### Method The method contract for `hubspot.fetch()` is as follows: ```js theme={null} import { hubspot } from '@hubspot/ui-extensions'; interface Options { method?: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'PATCH'; timeout?: number; body?: { [key: string | number]: unknown; } } hubspot.fetch(resource: string | URL): Promise hubspot.fetch(resource: string, options?: Options): Promise ``` | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `method` | String | The HTTP method to use. | | `timeout` | Number | Time in milliseconds to allow for the request to complete before timing out. Maximum value is 120000 (2 minutes). When not specified, the default timeout is 15 seconds. See [Limits](#limits) for retry behavior. | | `body` | Object | The request body. | **Please note:** for security reasons, you should not store secrets in your React code to communicate with third-party backends. Instead, use your own backend to fetch data from other third-party APIs after [validating the HubSpot signature](/docs/apps/legacy-apps/authentication/validating-requests) on the request. ### Headers and metadata To ensure that the requests hitting your backend are coming from HubSpot, several headers are included in the request. You can use these headers, along with the incoming request fields, to verify the signature of the request. Learn more about [validating HubSpot requests](/docs/apps/legacy-apps/authentication/validating-requests). HubSpot will always populate headers related to request signing and also allow you to pass a custom `Authorization` header from `hubspot.fetch()`. See the [example hubspot.fetch() request with Authorization header](#authorization-header) for more information. All other client-supplied request headers are stripped. Response headers from your backend are not returned. HubSpot also automatically adds the following query parameters to each request to supply metadata: * `userId` * `portalId` * `userEmail` * `appId` As the request URL is hashed as part of the signature header, this will help you securely retrieve the identity of the user making requests to your backend. Note the following: * While you can use `hubspot.fetch()` to pass an `Authorization` request header, `hubspot.fetch()` does not pass other client-supplied request headers or return response headers set by your backend server. * If you're not seeing appended metadata passed with `hubspot.fetch()` requests, check whether you have a `local.json` file that's currently rerouting requests via a proxy. If so, disable this local data fetch feature by renaming the file to `local.json.bak` and restarting the development server. ### Limits Requests made with `hubspot.fetch()` are subject to the following limits: * **Concurrency:** each app is allowed up to 20 concurrent requests per account. Additional requests are rejected with `429` and can be retried after a delay. * **Timeout:** each request has a default timeout of 15 seconds. It can be increased up to 120 seconds (2 minutes) using the `timeout` option. HubSpot will automatically retry a request once if there are issues establishing a connection, or if the request fails with a `5XX` status code within the timeout window. Request duration time includes the time required to establish an HTTP connection. * **Payload size:** both request and response payloads are limited to 1 MB. If you're familiar with using the [browser Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), note that `hubspot.fetch()` is not a one-to-one replacement. Key differences between the two APIs are listed below. | Feature | Native `fetch()` | `hubspot.fetch()` | | ---------------- | --------------------- | ----------------------------------- | | Custom headers | All headers supported | `Authorization` header only | | Response headers | Returned | Not returned | | Credentials mode | Supported | Not supported | | Cache control | Supported | Not supported | | Timeout | Browser default | 15 seconds default, 120 seconds max | | Payload size | Large | 1MB max | | URL requirements | Any URL | Must be in `permittedUrls` | ### Proxying requests to a locally running backend If you have a locally running backend, you can set up a proxy to remap `hubspot.fetch()` requests made during local development. This proxy is configured through a `local.json` file in your project, and will prevent requests from being routed through HubSpot's data fetch service. To proxy requests to a locally running backend: 1. Create a `local.json` file in the same directory as your `public-app.json` file. In this file, define a proxy that remaps requests made using `hubspot.fetch()`. This mapping will only happen for the locally running extension. You can include multiple proxy key-value pairs in the `proxy` object. ```json theme={null} { "proxy": { "https://example.com": "http://localhost:8080" } } ``` * Each proxy URL must be a valid URL and use HTTPS. * Path-based routing is not supported. For example, the following proxy won't work: `"https://example.com/a": "http://localhost:8080"` 2. Upload your project by running `hs project upload`. 3. With your project uploaded, run `hs project dev` to start the development server. The CLI should confirm that it has detected your proxy. ![public-app-local-proxy-detected](https://www.hubspot.com/hubfs/Knowledge_Base_2023_2024/public-app-local-proxy-detected.png) 4. When you request the mapped URL using `hubspot.fetch()`, the CLI will confirm the remapping. ![public-app-local-proxy-remapping](https://www.hubspot.com/hubfs/Knowledge_Base_2023_2024/public-app-local-proxy-remapping.png) #### Request signatures during proxied local development By default, when proxying requests during local development, requests will not be signed or include metadata in query parameters. However, if you want to introduce request signing into the local development process, you can inject the `CLIENT_SECRET` environment variable into the local development process. ##### Injecting CLIENT\_SECRET After setting up your `local.json` file to proxy specific domains, you can inject the `CLIENT_SECRET` variable when starting the local development server by prepending the `hs project dev` command with the variable: ```shell theme={null} CLIENT_SECRET="abc123" hs project dev ``` Note that this doesn't have to be a real `CLIENT_SECRET` value, as long as you inject the same variable in your locally running backend that you're using for `hs project dev`. For example, your backend might include the following Node or Python code: ```js JavaScript theme={null} CLIENT_SECRET="abc123" node my-app.js // and also CLIENT_SECRET="abc123" npm run dev ``` ```py Python theme={null} CLIENT_SECRET="abc123" python my-app.py ``` And to access the `CLIENT_SECRET` variable: ```js JavaScript theme={null} console.log(process.env.CLIENT_SECRET); ``` ```py Python theme={null} import os print(os.environ['CLIENT_SECRET']) ``` Once you've finalized your request signing logic and have permanently added it to your backend code, you'll need to inject the `CLIENT_SECRET` variable from your app into the `hs project dev` command permanently. For ease of use, consider baking the variable into your start scripts for local development. To validate HubSpot request signatures in your custom backend, check out the [request validation guide](/docs/apps/legacy-apps/authentication/validating-requests). You can also use HubSpot's [`@hubspot/api-client`](https://www.npmjs.com/package/@hubspot/api-client) npm module to verify requests and sign them yourself. For example: ```js theme={null} import { Signature } = from '@hubspot/api-client' const url = `${req.protocol}://${req.header('host')}${req.url}` const method = req.method; const clientSecret = process.env.CLIENT_SECRET const signatureV3 = req.header('X-HubSpot-Signature-v3'); const timestamp = req.header('X-HubSpot-Request-Timestamp'); // Reject the request if the timestamp is older than 5 minutes. if (parseInt(timestamp, 10) < (Date.now() - 5 * 60 * 1000)) { throw Error('Bad request. Timestamp too old.') } const requestBody = req.body === undefined || req.body === null ? '' : req.body; const validV3 = Signature.isValid({ signatureVersion: 'v3', signature: signatureV3, method, clientSecret, requestBody, url, timestamp, }); if (!validV3) { throw Error('Bad request. Invalid signature.') } ``` ### hubspot.fetch examples Below are examples of `hubspot.fetch` requests to illustrate basic and authorization header usage. #### Basic usage ```js theme={null} import React, { useEffect } from "react"; import { hubspot, Text } from "@hubspot/ui-extensions"; hubspot.extend(({ context }) => ); const Hello = ({ context }) => { useEffect(() => { const fetchData = async () => { const response = await hubspot.fetch("https://api.example.com/data", { timeout: 2_000, method: "GET", }); if (!response.ok) { throw new Error(`Request failed with status ${response.status}`); } const data = await response.json(); console.log(data); }; fetchData().catch(err => console.error("Something went wrong", err)); }, []); return Hello world; }; ``` #### Authorization header You may return a short-lived authorization token from your backend service after validating the HubSpot signature. You can then use this token to access other resources. To get the access token from your backend server in the UI extension: ```js wrap theme={null} hubspot.fetch(`${BACKEND_ENDPOINT}/get-access-token`, { timeout: 3000, method: 'GET', }) .then((response: Response) => { response.json().then((data) => setAccessToken(data.accessToken)); }) ``` To return a short-lived access token from your backend server: ```js wrap theme={null} app.get("/get-access-token", (req, res) => { validateHubspotSignatureOrThrow(req); res.json({ accessToken: generateShortLivedAccessToken(req.query.userEmail), expiryTime: addMinutes(currentTime, 10), }); }); ``` To attach access tokens to other UI extension requests: ```js theme={null} hubspot.fetch('https://www.oauth-enabled-api.com/', { timeout: 3000, method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }).then((response: Response) => { ... }) ``` ### Monitoring and logs To monitor `hubspot.fetch()` activity, you can view request logs in HubSpot: 1. In your HubSpot account, navigate to **Development**. 2. In the left sidebar, navigate to **Monitoring** > **Logs**. 3. On the monitoring page, click the **UI Extensions** tab. On the *UI Extensions* tab, use the **tabs** to view the various logs available. Learn more about [monitoring and logging for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring). The hs.fetch tab of the app monitoring page in HubSpot. # Logging and monitoring for UI extensions Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring Monitor and debug UI extensions during local development and after deployment using HubSpot's built-in logging and monitoring tools. HubSpot provides tools for monitoring and debugging UI extensions throughout the development lifecycle. During local development, the extension logs panel surfaces errors, warnings, and custom log messages directly in the browser. For deployed extensions, HubSpot's built-in monitoring page lets you review render logs, `hubspot.fetch()` requests, and custom log output. ## Logging during local development To help you identify and resolve issues before deploying a UI extension, HubSpot provides a logs panel that you can access from locally running UI extensions. The panel will surface errors, warnings, and other debugging information related to your extension code, such as invalid UI component prop values. Opening the extension log panel from an app card during local development ### Using the logs panel When an error or warning is detected in the locally running UI extension, a button will appear in the bottom right of the extension, which you can click to open the logs panel. The log panel open button displaying on an app card during local development. Each log entry includes the severity level and a message describing the issue. When applicable, log entries may include links to relevant documentation to help you resolve the issue. The panel will display up to 50 of the most recent logs collected during your current local development session. The logs panel open on a contact record, displaying an invalid prop error for the app card. ### Hiding the logs panel If you prefer not to see the logs panel during local development, you can hide it by setting the following value in your browser's `localStorage`: ```js theme={null} localStorage.setItem('hubspot::Extensions:logs:disabled', 'true') ``` To re-enable the logs panel, remove the key or set it to `'false'`: ```js theme={null} localStorage.removeItem('hubspot::Extensions:logs:disabled') ``` ### Custom log messages In addition to the logs that are automatically surfaced by the extension logs panel, you can send your own log messages using the [`logger` API](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/logging) provided by the UI extensions SDK. You can test out custom log messages using the example code below, which creates an extension with buttons to trigger specific errors: ```jsx wrap expandable theme={null} import React from "react"; import { Button, Divider, Flex, Text, hubspot, logger, } from "@hubspot/ui-extensions"; logger.warn("Warning in the middle tab, before my extension"); hubspot.extend(({ context }) => ); const MiddleTabLogging = ({ context }) => { logger.debug(JSON.stringify(context, null, 2)); const callFetchSuccess = async () => { try { const response = await hubspot.fetch("https://jsonplaceholder.typicode.com/posts/1", { method: "GET" }); const result = await response.json(); logger.info(JSON.stringify(result, null, 2)); } catch (error) { logger.error(error.message); } }; const callFetchFail = async () => { try { const response = await hubspot.fetch("https://jsonplaceholder.typicode.com/posts/404", { method: "GET" }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); logger.info(JSON.stringify(result, null, 2)); } catch (error) { logger.error(error.message); } }; return ( Test out the logger with the following buttons. The browser's developer console will show your events in local dev. Test fetch functions Test different log levels. Deploy the app and crash the card. Use the Trace ID to see what happened in the Log Traces tab in your private app's dashboard. ); }; ``` ## Monitoring deployed extensions To monitor UI extension activity, you can view request logs in HubSpot: * In your HubSpot account, navigate to **Development**. * In the left sidebar, navigate to **Monitoring** > **Logs**. * On the monitoring page, click the **UI Extensions** tab. The monitoring page for UI extensions in HubSpot. On the *UI Extensions* tab, use the various **tabs** to view the different log types: * **Extension Render**: logs related to UI extensions loading in their configured locations. * **hubspot.fetch()**: logs related to `hubspot.fetch()` requests. * **Extension Log**: custom messages logged via the [logger API](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/logging). The UI Extensions tab found in the monitoring page. To view more information about a log entry, click the **ellipsis** button in the *Actions* column of the row, then select an action: * **Open details:** review the details for the entry and use the provided IDs for deeper debugging. * **Open tracing:** view the complete tracing details for the log entry. Detail options for events in the monitoring page. # UI extensions overview Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/overview Learn about the different types of UI extensions and how to build them. HubSpot UI extensions allow you to build rich, contextual custom interfaces in HubSpot. Tailor CRM records, workflows, and app experiences with dynamic data, custom layouts, and interactive React-based components, all powered by the [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/overview). Below, learn more about when to use UI extensions, how they work, and how to get started. If you have an existing legacy CRM card, check out HubSpot's [Legacy CRM Card to UI Extension Converter](https://github.com/HubSpot/ui-extensions-examples/tree/main/legacy-card-converter), which provides an example implementation of an app card that functions like a legacy CRM card. ## When to use UI extensions UI extensions are ideal when: * You want to display external system data in HubSpot. * You need to guide users through multi-step flows. * You want to enable users to customize CRM records with richer functionality than HubSpot's default UI allows. * You want HubSpot to host your app's configuration UI. * You need a secure and scalable way to embed custom UI logic without hosting your own frontend architecture. ## How UI extensions work At a high level, a UI extension consists of: * A JSON configuration file (`*-hsmeta.json`) that registers where the extension appears in HubSpot and specifies the entry point. * An entry point (`hubspot.extend()`) that initializes the extension and receives context and actions from HubSpot. * A React component (`.jsx` or `.tsx`) that defines your UI. TypeScript is fully supported and recommended for better type safety. * Optional data fetching via the `hubspot.fetch()` API to request data from external services. During runtime: 1. HubSpot loads your extension in a secure, sandboxed environment. 2. Context is passed into your component, including information about the current user, HubSpot account, and (for CRM extensions) the current record's ID and object type. 3. Your component can fetch CRM data using [SDK hooks](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/hooks) or call external services using `hubspot.fetch()`. 4. The UI renders and reacts to user interactions. This architecture ensures secure isolation, predictable performance, and a consistent developer experience. UI extensions are subject to the following limitations: * Each `hubspot.fetch()` request has a default timeout of 15 seconds, configurable up to 120 seconds (2 minutes) using the `timeout` option. * Request and response payloads are limited to 1 MB each. * Each app is allowed up to 20 concurrent `hubspot.fetch()` requests per account. * `hubspot.fetch()` does not support custom request or response headers (except for the `Authorization` header). ## What you can build ### App cards Create app cards to surface insights, trigger external workflows, or bring data from third-party services directly into HubSpot. You can build app cards for [CRM records](https://knowledge.hubspot.com/records/view-and-filter-records), [preview panels](https://knowledge.hubspot.com/records/preview-a-record), and the [help desk](https://knowledge.hubspot.com/help-desk/overview-of-the-help-desk-workspace) sidebar. Common use cases include: * Displaying enriched company or contact data. * Embedding task lists, timeline details, or customer insights. * Triggering external workflows or actions from within a record. * Rendering custom layouts, accordions, tables, and charts. Learn more about [building app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview). Apps that use [Sensitive Data scopes](/docs/api-reference/latest/crm/properties/sensitive-data) can include app cards, but cannot use `hubspot.fetch()` or serverless functions. Learn more about [Sensitive Data restrictions for app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/overview#sensitive-data-scopes). Example screenshot of a deal stage tracker card ### App home pages A home page is a full-screen extension, ideal for dashboards, analytics views, and complex workflows. Common use cases include: * Building analytics dashboards. * Enabling multi-step flows or guided processes. * Aggregating cross-object CRM insights. * Creating custom workspace experiences. Learn more about [building app home pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview). Screenshot of an example app home page ### Settings pages Settings pages allow you to build configuration UIs that live inside HubSpot’s settings. These pages are ideal for: * Managing API keys or OAuth credentials. * Managing feature toggles or custom configuration. * Connecting external services. * Mapping CRM properties to external systems. Learn more about [building app settings pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page). Screenshot of an example app settings page ## Building UI extensions All UI extensions share the same development workflow: 1. **Create the extension:** run the `hs project add` command to add a card, home page, or settings page component to an existing project. Or, follow the [quickstart](/docs/getting-started/quickstart) to build your first extension. 2. **Build with React:** build your extension's UI with React or TypeScript, using HubSpot's [UI component library](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) to render elements. 3. **Register with the SDK:** use `hubspot.extend()` to register your extension with HubSpot. 4. **Upload and test:** run `hs project upload` to deploy, and `hs project dev` for local development. You can also build testing into your project using a set of provided [testing utilities](#local-development-and-testing). ### Supported locations UI extensions can appear in specific locations throughout HubSpot. By default, an app card is eligible for every surface that's compatible with its card type and `objectTypes`, and account admins choose where to place it. To restrict a card to one surface, set the `location` field in its JSON configuration file: ```json highlight={7} theme={null} { "uid": "example-card", "type": "card", "config": { "name": "Hello Example App", "description": "A description of the card's purpose.", "location": "crm.record.tab", "entrypoint": "/app/cards/ExampleCard.jsx", "objectTypes": ["contacts"] } } ``` | Location | Value | Description | | ------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CRM record middle column | `crm.record.tab` | Appears in the middle column of CRM record pages, within default or custom tabs. Also available in the sales workspace target accounts preview panel when `objectTypes` is `COMPANIES`. | | CRM record sidebar | `crm.record.sidebar` | Appears in the right sidebar of CRM record pages. Also available in the sales workspace deals sidebar when `objectTypes` is `DEALS`. | | CRM preview panel | `crm.preview` | Appears in the preview panel accessible throughout the CRM, including record pages, index pages, board views, and the lists tool. | | Help desk sidebar | `helpdesk.sidebar` | Appears in ticket sidebars within help desk, including the ticket preview panel and the right sidebar of ticket views. | | App pages | `home` | Appears in the [app home page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages). | | App settings page | `settings` | Appears in the [app settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page). | Learn more about supported objects and location configuration in the [app cards reference](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/reference#card-locations). ### The UI extensions SDK The [UI extensions SDK](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/overview) provides the foundation for all UI extensions, including: * **Context access**: retrieve information about the current user, account, and CRM record. * **Actions**: display alerts, copy text to clipboard, reload pages, and open overlays. * **Hooks**: fetch extension metadata, CRM properties, and associations. ### Fetching external data Use the `hubspot.fetch()` API to request data from external services. Learn more about [fetching data for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data), including: * Configuring permitted URLs. * Request signing and validation. * Proxying requests during local development. ### UI components HubSpot provides a library of reusable components for building extension interfaces, including: * **[Standard components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview#standard-components)**: general-purpose components like buttons, forms, tables, and layout components. Imported from `@hubspot/ui-extensions`. * **[CRM data components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-data-components/overview)**: components that automatically fetch and display data from CRM records, such as property lists, association tables, and reports. Imported from `@hubspot/ui-extensions/crm`. * **[CRM action components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/crm-action-components/overview)**: components that trigger built-in CRM actions like adding notes, sending emails, or creating records. Imported from `@hubspot/ui-extensions/crm`. Check out HubSpot's [Figma Design Kit](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/figma-design-kit) to help you create more detailed designs, share demos with stakeholders, and keep teams aligned during the development process. ### Local development and testing Use the `hs project dev` command to start a local development server with hot reload. This enables you to preview changes to your extension in real-time without needing to upload your project after every change. Screenshot showing an app card on a contact record with the local development indicator To help debug during local development, HubSpot provides an extension logs panel that surfaces errors, warnings, and custom log messages directly in the browser. Log panel displaying custom log messages. For deployed extensions, you can review render logs, `hubspot.fetch()` requests, and custom log output. Learn more about [logging and monitoring for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/logging-and-monitoring). The UI extensions SDK also provides [testing utilities](/docs/apps/developer-platform/add-features/ui-extensions/tools/testing/overview) for writing unit tests, including: * Rendering components in a test environment. * Querying and interacting with rendered output. * Mocking context, actions, and CRM data hooks. * Debugging rendered component trees. ### Code quality and linting HubSpot provides an ESLint configuration specifically designed for UI extensions. This catches common issues like using unavailable browser APIs and incorrect imports before runtime. Learn more about [linting for UI extensions](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview). ## Next steps * [Quickstart](/docs/getting-started/quickstart): create your first app on the developer platform. * [Create an app card](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card): create a custom card that displays on CRM records. * [Create a home page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/overview): build a landing page for your app. * [Create a settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page): add a configuration interface for your app. # Share code between your extensions using npm workspaces Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/code-sharing-with-npm-workspaces Learn how to use npm workspaces to share code between your UI extensions. As you build out your UI extensions, you can leverage [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces/) to share code across your [app cards](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-cards/create-an-app-card), [settings page](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/create-a-settings-page), [app pages](/docs/apps/developer-platform/add-features/ui-extensions/extension-points/app-pages/create-app-pages), and [serverless functions](/docs/apps/developer-platform/add-features/serverless-functions/overview). This allows you to consolidate common logic such as formatting dates and currency, and ensure that any fixes or updates are rolled out consistently to each UI extension. ## Prerequisites Before getting started, install the latest version of the [HubSpot CLI](/docs/developer-tooling/local-development/hubspot-cli/install-the-cli). In a terminal window, run the following command: ```shell theme={null} npm install -g @hubspot/cli ``` The CLI needs to be version `8.0.0` or later. * You can check which version of the CLI you have by running `hs --version`. * If needed, you can run the command `npm install -g @hubspot/cli@latest` to update to the latest version of the HubSpot CLI. ## Project structure The code for your card, setting page, and home page follows the same directory structure as outlined in the [app configuration article](/docs/apps/developer-platform/build-apps/app-configuration). The main change you'll need to make to set up code sharing between your extensions will be to add a `package.json` file at the root of your project under `src/app/`. The naming convention and scope you choose for these shared packages is entirely customizable, allowing you to use a pattern you're already comfortable with, such as `@packages/types`. An example directory structure that demonstrates the root `package.json` location, along with the shared code for `types`, `utils`, and `components` is shown below: ```shell highlight={6-16} theme={null} my-hubspot-project/ └── hsproject.json └── src/ └── app/ └── app-hsmeta.json └── package.json └── packages/ └── types/ └── package.json └── index.ts └── utils/ └── package.json └── index.ts └── components/ └── package.json └── index.tsx └── cards/ └── MyCard.jsx └── my-app-card-hsmeta.json └── package.json └── settings/ └── Settings.tsx └── settings-hsmeta.json └── package.json └── pages/ └── Home.tsx └── package.json ``` ## Configure npm workspaces After you create a `package.json` file in the `src/app/` directory of your project, open the `package.json` file and define the `name` and `workspaces` fields: * `name`: the name of your project (e.g., `my-hubspot-project`). * `workspaces`: an array that includes the UI extension directories where you plan to use your shared code, along with the directory where your shared code lives (e.g., `packages/*`). For example, following the same directory structure shown above, the resulting `package.json` file would be: ```json theme={null} { "name": "my-hubspot-project", "workspaces": ["cards", "settings", "pages", "packages/*"] } ``` ## Create shared packages You can use any naming convention when defining the package scope for your shared code (e.g., `@my-hubspot-project` in the examples below). The code blocks below provide examples for defining shared `types`, `utils`, and `components` that you can then import into your UI extensions. ### Types The examples below define a simple `Money` type under `packages/types` in your project. ```json theme={null} { "name": "@my-hubspot-project/types", "private": true, "version": "1.0.0", "main": "index.ts" } ``` ```js theme={null} export interface Money { amount: number; currency: 'USD' | 'EUR' | 'GBP'; } ``` ### Utilities The examples below define a utility under `packages/utils` that will format data using the `Money` type from above. ```json theme={null} { "name": "@my-hubspot-project/utils", "private": true, "version": "1.0.0", "main": "index.ts", "dependencies": { "@my-hubspot-project/types": "1.0.0" } } ``` ```js theme={null} import { Money } from "@my-hubspot-project/types"; export function formatDate(date: Date | string): string { const d = typeof date === 'string' ? new Date(date) : date; return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); } export function formatMoney(money: Money): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: money.currency, }).format(money.amount); } ``` ### Components The examples below demonstrate a shared component that lives under `packages/components`. ```json theme={null} { "name": "@my-hubspot-project/components", "private": true, "version": "1.0.0", "main": "index.tsx", "dependencies": { "@hubspot/ui-extensions": "latest", "react": "^18.2.0" } } ``` ```js theme={null} import { Text, Flex, Divider } from "@hubspot/ui-extensions"; interface SectionProps { title: string; children: React.ReactNode; } export const Section = ({ title, children }: SectionProps) => ( {title} {children} ); ``` ## Add dependencies to extensions After you've defined your shared code under `packages/`, add the path to your shared packages in each of your extension's respective `package.json` files: ```json theme={null} { "name": "@my-hubspot-project/my-cards", "dependencies": { "@hubspot/ui-extensions": "latest", "@my-hubspot-project/types": "1.0.0", "@my-hubspot-project/components": "1.0.0", "@my-hubspot-project/utils": "1.0.0" } } ``` ## Use shared code in your UI extensions The following code block shows how to use your shared code within one of your UI extensions: ```jsx theme={null} import { hubspot } from "@hubspot/ui-extensions"; import { Text } from "@hubspot/ui-extensions"; import { Money } from "@my-hubspot-project/types"; import { Section } from "@my-hubspot-project/components"; import { formatDate, formatMoney } from "@my-hubspot-project/utils"; const dealAmount: Money = { amount: 15000, currency: 'USD' }; hubspot.extend<'crm.record.tab'>(({ context }) => (
Amount: {formatMoney(dealAmount)} Close Date: {formatDate('2024-03-15')}
)); ``` After you've added the imports of your shared code in your UI extensions, you can proceed with the next step of installing the shared packages so the project will build. ## Install shared packages To ensure your shared packages can be imported successfully in your UI extension, run the following command anywhere in your project: ```shell theme={null} hs project install-deps ``` Alternatively, your shared packages will automatically be installed next time you run `hs project dev` or `hs project upload`. # Migrating to v1.0 (ESLint 9) Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/migrate Migrate an existing ESLint configuration to ESLint 9. Version 1.0 is a major update that moves to ESLint 9's [flat config format](https://eslint.org/docs/latest/use/core-concepts/glossary#flat-config) and unbundles third-party rules to give you more control over your linting setup. The flat config files don't allow nesting configuration files in subdirectories -- instead, all nesting must be in one configuration file. If you already have an ESLint configuration in your project, review the table below to see what's changed: | Before (v0.x) | After (v1.0) | | ----------------------------------------------- | ----------------------------------- | | ESLint 8 | ESLint 9+ required | | Legacy config (`.eslintrc.*`) | Flat config (`eslint.config.js`) | | Bundled `eslint:recommended` | Install separately via `@eslint/js` | | Bundled `eslint-plugin-react` recommended rules | Install and configure separately | | Bundled `eslint-plugin-react-hooks` | Install and configure separately | | Bundled `eslint-config-prettier` | Install and configure separately | | CommonJS (`require()`) | ESM (`import`) | After reviewing the above changes, proceed with the steps below to migrate to the latest version. ## Migrate your ESLint config ### 1. Review your current config Before migrating, open your existing `.eslintrc.*` file and note: * Any custom `rules` you've added or modified * Any additional `plugins` you're using * Any `overrides` for specific file patterns * Any `env` or `globals` settings You'll need to translate these to flat config format. Keep your old config file for reference until migration is complete. ### 2. Update dependencies Upgrade to ESLint 9 and the new package version by running the following command: ```shell theme={null} npm i --save-dev eslint@^9 @hubspot/eslint-config-ui-extensions@^1 ``` ### 3. Set up ESM ES modules is required for a flat configuration, so you'll need to ensure your `package.json` includes `"type": "module"`: ```json theme={null} { "type": "module" } ``` ### 4. Create a new ESLint config file Create an `eslint.config.js` file in your project root. Start with either the minimal or recommended setup below, then add your custom rules. Read more about each addition in the [Recommended setup section of the overview](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview#recommended-setup). **Minimal setup** (HubSpot UI extensions rules only): ```js theme={null} import { config } from '@hubspot/eslint-config-ui-extensions'; export default [ ...config, // Add your custom rules here (see Step 5) ]; ``` **Recommended setup** (replaces previously bundled configs, plus extras): ```shell theme={null} npm i --save-dev @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier eslint-plugin-unused-imports ``` ```js theme={null} import { defineConfig } from 'eslint/config'; import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import unusedImports from 'eslint-plugin-unused-imports'; import prettier from 'eslint-config-prettier'; import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions'; export default defineConfig([ js.configs.recommended, tseslint.configs.recommended, tseslint.configs.stylistic, react.configs.flat.recommended, react.configs.flat['jsx-runtime'], { settings: { react: { version: 'detect' }, }, }, ...uiExtensionsConfig, reactHooks.configs.flat['recommended-latest'], { plugins: { 'unused-imports': unusedImports, }, rules: { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off', 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ 'warn', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_', }, ], 'react/prop-types': 'off', }, }, prettier, // Add your custom rules here (see Step 5) ]); ``` ### 5. Migrate your custom configuration Refer to your old config file and translate your customizations to flat config format. Custom rules: ```js theme={null} // Before (.eslintrc.js) module.exports = { rules: { 'no-console': 'warn', 'prefer-const': 'error', }, }; // After (eslint.config.js) - add to your config array { rules: { 'no-console': 'warn', 'prefer-const': 'error', }, } ``` Overrides (file-specific rules): ```js theme={null} // Before (.eslintrc.js) module.exports = { overrides: [ { files: ['*.test.js'], rules: { 'no-console': 'off', }, }, ], }; // After (eslint.config.js) - add to your config array { files: ['**/*.test.js'], rules: { 'no-console': 'off', }, } ``` Globals: ```js theme={null} // Before (.eslintrc.js) module.exports = { globals: { myGlobal: 'readonly', }, }; // After (eslint.config.js) - add to your config array { languageOptions: { globals: { myGlobal: 'readonly', }, }, } ``` Environments: Flat config uses `globals` instead of `env`. ```js theme={null} // Before (.eslintrc.js) module.exports = { env: { browser: true, node: true, }, }; // After (eslint.config.js) import globals from 'globals'; export default [ { languageOptions: { globals: { ...globals.browser, ...globals.node, }, }, }, // ... rest of config ]; ``` For more details, see the [official ESLint migration guide](https://eslint.org/docs/latest/use/configure/migration-guide). ### 6. Testing and cleanup Run ESLint to verify your migration: ```shell theme={null} npm run lint ``` Once you've verified that everything works, you can delete your old `.eslintrc.*` file. ## Troubleshooting ### ESLint seems to ignore your new config ESLint 9 looks for `eslint.config.js` by default and ignores `.eslintrc.*` files. If it seems like ESLint is ignoring your new config, make sure: 1. You created `eslint.config.js` in your project root. 2. The file is correctly exporting an array (check for syntax errors). ### Error: Cannot use require() to import an ES module Your config file is being loaded as CommonJS. Make sure: 1. Your `package.json` has `"type": "module"`. 2. Your config file is named `eslint.config.js` (not `.cjs`). ### Errors about missing plugins or rules If you see errors like `Definition for rule 'react/prop-types' was not found`, you're using a rule from a plugin that was previously bundled but is now opt-in. Either: 1. Install the plugin and add it to your config (see [recommended setup](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview#recommended-setup)). 2. Remove the rule if you no longer need it. ### Error: Invalid option 'extends' Flat config uses a different structure than legacy config. You can't use `extends`, `env`, `plugins` (as an array), or `overrides` the same way. See [Step 5 above](#5-migrate-your-custom-configuration) for translation examples. # Linting for UI extensions Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/overview An overview of ESLint shareable configurations for building UI extensions on HubSpot with React. For security and reliability, UI extensions run in a sandboxed web worker environment with restricted APIs. This can cause issues or unexpected behavior when developing UI extensions, especially if you're not fully familiar with the UI extensions environment. The `@hubspot/eslint-config-ui-extensions` package provides ESLint rules specifically designed for HubSpot UI extensions, and catches common issues like using unavailable browser APIs, incorrect imports, and other patterns that won't work in the sandboxed web worker context. It's recommended to set up ESLint with this configuration when starting a new UI extensions project, but can be implemented at any time. ## Automatic configuration If you don't yet have linting configured and want to set it up automatically, you can run the `hs project lint` command from the root of your project to set up the necessary configuration files and dependencies. ## Manual installation **Upgrading from v0.x?** See the [migration guide](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/migrate) for step-by-step instructions. Install [ESLint](https://eslint.org/) 9+ alongside this package: ```shell theme={null} npm i --save-dev eslint@^9 @hubspot/eslint-config-ui-extensions ``` ## Basic usage Create an `eslint.config.js` file in the root of your project: ```js theme={null} import { config } from '@hubspot/eslint-config-ui-extensions'; export default [ ...config, // Your project-specific overrides ]; ``` Make sure your `package.json` has `"type": "module"` set, then add a lint script: ```json theme={null} { "type": "module", "scripts": { "lint": "eslint ." } } ``` Run `npm run lint` to check for linting issues. ## Recommended setup The basic config above only includes HubSpot UI extensions-specific rules. For a production-ready setup, it's recommended that you add standard ESLint rules, TypeScript support, React Hooks linting, Prettier compatibility, and unused import detection. ### Install additional dependencies ```sh theme={null} npm i --save-dev @eslint/js typescript-eslint eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier eslint-plugin-unused-imports ``` ### Full config ```js theme={null} import { defineConfig } from 'eslint/config'; import js from '@eslint/js'; import tseslint from 'typescript-eslint'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import unusedImports from 'eslint-plugin-unused-imports'; import prettier from 'eslint-config-prettier'; import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions'; export default defineConfig([ // Standard ESLint recommended rules js.configs.recommended, // TypeScript support with stylistic rules tseslint.configs.recommended, tseslint.configs.stylistic, // React recommended rules + JSX runtime support (React 17+) react.configs.flat.recommended, react.configs.flat['jsx-runtime'], { settings: { react: { version: 'detect' }, }, }, // HubSpot UI extensions rules ...uiExtensionsConfig, // React Hooks rules reactHooks.configs.flat['recommended-latest'] // Unused imports detection (auto-fixable with --fix) { plugins: { 'unused-imports': unusedImports, }, rules: { 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off', 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ 'warn', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_', }, ], 'react/prop-types': 'off', }, }, // Prettier compatibility (must be last to override other configs) prettier, ]); ``` ### What each addition provides | Package | Purpose | | :----------------------------- | :--------------------------------------------------------------------------------------------------------- | | `@eslint/js` | ESLint's built-in recommended rules for catching common JavaScript issues | | `typescript-eslint` | TypeScript parsing and linting rules (`stylistic` adds consistent code style enforcement) | | `eslint-plugin-react` | React-specific rules; `jsx-runtime` config is for React 17+ (no need to import React in every file) | | `eslint-plugin-react-hooks` | Enforces [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) and verifies dependency arrays | | `eslint-config-prettier` | Disables ESLint rules that conflict with Prettier formatting | | `eslint-plugin-unused-imports` | Auto-fixable detection and removal of unused imports | ### JavaScript-only projects If you're not using TypeScript, you can simplify the config: ```js theme={null} import { defineConfig } from 'eslint/config'; import js from '@eslint/js'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import unusedImports from 'eslint-plugin-unused-imports'; import prettier from 'eslint-config-prettier'; import { config as uiExtensionsConfig } from '@hubspot/eslint-config-ui-extensions'; export default defineConfig([ js.configs.recommended, react.configs.flat.recommended, react.configs.flat['jsx-runtime'], { settings: { react: { version: 'detect' }, }, }, ...uiExtensionsConfig, reactHooks.configs.flat['recommended-latest'], { plugins: { 'unused-imports': unusedImports, }, rules: { 'no-unused-vars': 'off', 'unused-imports/no-unused-imports': 'error', 'unused-imports/no-unused-vars': [ 'warn', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_', }, ], 'react/prop-types': 'off', }, }, prettier, ]); ``` ## Rules The rules included in the shared config from `@hubspot/eslint-config-ui-extensions` are enabled by default, and should always be enabled when building UI extensions. | Name | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [no-browser-dialogs](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-dialogs) | Prevent usage of native browser dialog APIs (alert, confirm, and prompt) in UI extensions. | | [no-browser-storage](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-storage) | Prevent usage of browser storage APIs (`localStorage` and `sessionStorage`) in UI extensions, which run in a sandboxed web worker environment where these APIs are not available. | | [no-console](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-console) | Prevent usage of console methods in UI extensions in favor of the SDK logger utility. | | [no-dom-access](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-dom-access) | Prevent access to the DOM (`document` object) in UI extensions, which run in a sandboxed web worker without DOM access. | | [no-html-elements](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-html-elements) | Disallow usage of unsupported HTML elements in UI extensions. | | [no-invalid-extension-point-imports](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-invalid-extension-point-imports) | Prevent importing components from unsupported extension points. | | [no-invalid-image-src](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-invalid-image-src) | Only allow valid image URLs in the src attribute of Image components from @hubspot/ui-extensions. | | [no-native-http](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-native-http) | Prevent usage of native browser HTTP APIs (fetch and XMLHttpRequest) in UI extensions. | | [no-parent-imports](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-parent-imports) | Prevent importing files from outside the extension point root directory. This rule can be automatically fixed by the [`--fix` CLI option](https://eslint.org/docs/latest/use/command-line-interface#--fix). | | [no-restricted-globals](/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-restricted-globals) | Prevent usage of browser globals that are not available in the sandboxed web worker environment. | # no-browser-dialogs Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-dialogs Prevent usage of native browser dialog APIs (alert, confirm, and prompt) in UI extensions. The `no-browser-dialogs` rule prevents usage of the following native browser dialog APIs: [alert()](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [confirm()](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), and [prompt()](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt). ## Rule details UI extensions run in a sandboxed web worker environment where native browser dialog APIs (`alert()`, `confirm()`, and `prompt()`) are intentionally replaced with error-throwing functions for security reasons. These blocking dialogs would freeze the entire UI and negatively impact user experience. Instead, UI extensions provide non-blocking alternatives that integrate seamlessly with the HubSpot interface. ## Dialog method alternatives Use the following UI extension alternatives instead of browser dialog APIs: | Native API | Alternative | Purpose | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `alert()` (for pop-ups) | [`actions.addAlert()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) | Display toast notifications | | `alert()` (for inline alerts) | [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) component | Display inline alerts | | `confirm()` | [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) + [` } > Delete Item ); }; ``` ### Prompts Instead of using `prompt()`: ```jsx theme={null} // Incorrect function getUserInput() { const name = prompt('Enter your name:'); return name; } // Also incorrect const value = self.prompt('Enter a value:', 'default'); ``` Use a combination of [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) and [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/input) components: ```jsx theme={null} import { Button, Input, Modal, ModalBody, ModalFooter, Text, } from '@hubspot/ui-extensions'; import { useState } from 'react'; const Extension = ({ actions }) => { const [inputValue, setInputValue] = useState(''); const handleSubmit = () => { actions.addAlert({ type: 'success', message: `You entered: ${inputValue}`, }); }; return ( } > Get Input ); }; ``` ## Related resources * [actions.addAlert()](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) * [Alert Component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) * [Modal Component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/modal) # no-browser-storage Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-browser-storage Prevent usage of browser storage APIs (localStorage and sessionStorage) in UI extensions. The `no-browser-storage` rule prevents usage of browser storage APIs ([localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage)). ## Rule details UI extensions run in a sandboxed web worker environment where browser storage APIs (`localStorage` and `sessionStorage`) are intentionally unavailable for security and isolation. This prevents extensions from accessing storage data from the host application, storing sensitive data without proper security controls, or interfering with other extensions. Storage APIs don't exist at runtime and will throw errors if accessed. Instead, use React state for temporary data or backend APIs for persistent data. ## Storage alternatives Use the following UI extension alternatives instead of browser storage APIs: | Use Case | Alternative | Purpose | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------- | | Temporary session data | [React state](https://react.dev/learn/managing-state) (`useState`, `useReducer`) | Store component state | | Persistent user data | [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) + backend API | Store data across sessions | | Complex app state | [React Context](https://react.dev/reference/react/createContext) + `useReducer` | Manage shared state | | API response caching | React state with [custom hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) | Cache data in memory | See the next section for an example of each alternative. ## Examples ### Temporary data Instead of using `sessionStorage` for temporary form data: ```jsx theme={null} // Incorrect function cacheApiResponse(endpoint, data) { sessionStorage.setItem(`cache:${endpoint}`, JSON.stringify(data)); } const Extension = () => { const cached = sessionStorage.getItem('formData'); return {cached}; }; ``` Use React state: ```jsx theme={null} import { useState } from 'react'; import { Input, Button, Flex } from '@hubspot/ui-extensions'; const Extension = () => { const [formData, setFormData] = useState({ name: '', email: '' }); return ( setFormData({ ...formData, name: value })} /> setFormData({ ...formData, email: value })} /> ); }; ``` ### Persistent data Instead of using `localStorage` for persistent user preferences: ```jsx theme={null} // Incorrect const savePreferences = (theme) => { localStorage.setItem('userPrefs', JSON.stringify({ theme })); }; const Extension = () => { const [theme, setTheme] = useState(() => { const saved = localStorage.getItem('userPrefs'); return saved ? JSON.parse(saved).theme : 'light'; }); return Theme: {theme}; }; ``` Use [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) to persist data via your backend: ```jsx theme={null} import { useState, useEffect } from 'react'; import { hubspot, logger } from '@hubspot/ui-extensions'; import { Button, Text, LoadingSpinner, Flex } from '@hubspot/ui-extensions'; const Extension = () => { const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const loadSettings = async () => { try { const response = await hubspot.fetch('/api/user/preferences'); const data = await response.json(); setSettings(data); } catch (error) { logger.error('Failed to load settings', error); } finally { setLoading(false); } }; loadSettings(); }, []); const updateSetting = async (key, value) => { try { await hubspot.fetch('/api/user/preferences', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ [key]: value }), }); setSettings({ ...settings, [key]: value }); } catch (error) { logger.error('Failed to update setting', error); } }; if (loading) return ; return ( Theme: {settings?.theme || 'default'} ); }; ``` ### Complex state management Instead of using `localStorage` for app-wide state: ```jsx theme={null} // Incorrect const AppState = { save: (key, value) => { const state = JSON.parse(localStorage.getItem('appState') || '{}'); state[key] = value; localStorage.setItem('appState', JSON.stringify(state)); }, get: (key) => { const state = JSON.parse(localStorage.getItem('appState') || '{}'); return state[key]; } }; ``` Use React Context with `useReducer`: ```jsx theme={null} import { createContext, useContext, useReducer } from 'react'; import { hubspot } from '@hubspot/ui-extensions'; import { Button, Text } from '@hubspot/ui-extensions'; const AppStateContext = createContext(); const initialState = { cache: {}, settings: {}, }; function stateReducer(state, action) { switch (action.type) { case 'CACHE_DATA': return { ...state, cache: { ...state.cache, [action.key]: action.data } }; case 'UPDATE_SETTINGS': return { ...state, settings: { ...state.settings, ...action.payload } }; default: return state; } } const AppStateProvider = ({ children }) => { const [state, dispatch] = useReducer(stateReducer, initialState); return ( {children} ); }; const DataComponent = () => { const { state, dispatch } = useContext(AppStateContext); const cacheData = (key, data) => { dispatch({ type: 'CACHE_DATA', key, data }); }; return ( <> Cached items: {Object.keys(state.cache).length} ); }; hubspot.extend(() => ( )); ``` ## Related resources * [UI extensions SDK overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk) * [hubspot.fetch() API](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) * [React state management](https://react.dev/learn/managing-state) # no-console Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-console Prevent usage of console methods in UI extensions in favor of the SDK logger utility. The `no-console` rule prevents usage of native [console](https://developer.mozilla.org/en-US/docs/Web/API/console) methods ([`console.log()`](https://developer.mozilla.org/en-US/docs/Web/API/console/log_static), [`console.error()`](https://developer.mozilla.org/en-US/docs/Web/API/console/error_static), etc.) in UI extensions. ## Rule details UI extensions should use the SDK's structured [`logger`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging) utility instead of native `console.*` methods. This helps streamline UI extension debugging, as you won't need to sort through other framework logs and network activity to find extension-specific logs. The SDK logger integrates with HubSpot's logging infrastructure, providing proper log levels, formatting, and the ability to filter and search across installs. Using `console.*` methods bypasses these benefits. ## Console method alternatives Use the following UI extension alternatives instead of browser console methods: | Console API | SDK Logger Alternative | Purpose | | ----------------- | ---------------------- | --------------------- | | `console.log()` | `logger.debug()` | Debug-level logging | | `console.info()` | `logger.info()` | Informational logging | | `console.warn()` | `logger.warn()` | Warning-level logging | | `console.debug()` | `logger.debug()` | Debug-level logging | | `console.error()` | `logger.error()` | Error-level logging | **For user-facing errors:** use [`actions.addAlert()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) or the [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) component instead of console output. See the next section for an example of each alternative. ## Examples ### Basic logging Instead of using `console.*` methods for logging: ```jsx theme={null} // Incorrect function debugData(data) { console.log('Fetched data:', data); } window.console.log('Component mounted'); console.info('User logged in'); console.warn('Deprecated feature used'); console.debug('Debug information'); ``` Use the SDK's [`logger`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging): ```jsx theme={null} import { hubspot, logger, Text } from '@hubspot/ui-extensions'; const Extension = () => { const debugData = (data) => { logger.debug('Fetched data:', data); }; logger.info('User logged in'); logger.warn('Deprecated feature used'); return Content here; }; export default hubspot.extend(() => ); ``` ### Error handling Instead of using `console.error()`: ```jsx theme={null} // Incorrect function handleError(error) { console.error('An error occurred:', error); } globalThis.console.error('Failed to load data'); ``` Use [`logger.error()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging) for debugging and [`actions.addAlert()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) for user-facing errors: ```jsx theme={null} import { Text, hubspot, logger } from '@hubspot/ui-extensions'; const Extension = ({ actions }) => { const handleError = (error) => { // Log for debugging logger.error('Failed to save data:', error); // Show user-friendly message actions.addAlert({ type: 'danger', message: 'Unable to save. Please try again.', }); }; return Content here; }; export default hubspot.extend(({ actions }) => ( )); ``` Or use the [``](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) component for inline error messages: ```jsx theme={null} import { Alert, Text, hubspot, logger } from '@hubspot/ui-extensions'; import { useState } from 'react'; const Extension = () => { const [error, setError] = useState(null); const handleAction = async () => { try { // Some operation... } catch (err) { logger.error('Operation failed:', err); setError('Unable to complete the operation. Please try again.'); } }; return ( <> {error && ( {error} )} {/* Rest of component */} ); }; export default hubspot.extend(() => ); ``` ## Related resources * [SDK logger](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#logging) * [actions.addAlert()](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#display-alert-banners) * [Alert component](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/standard-components/alert) # no-dom-access Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-dom-access Learn more about the `no-dom-access` ESLint rule, provided by the `@hubspot/eslint-config-ui-extensions` package. The `no-dom-access` rule prevents usage of the [document](https://developer.mozilla.org/en-US/docs/Web/API/Document) object and DOM APIs in UI extensions. ## Rule details UI extensions run in a sandboxed web worker environment where the DOM is intentionally unavailable. This architectural constraint ensures security and stability by preventing extensions from accessing sensitive host application data, modifying the main HubSpot interface, or interfering with other extensions. The `document` object and DOM APIs don't exist at runtime. Attempting to use them will cause runtime errors. Instead, build your UI entirely with React components and hooks from [`@hubspot/ui-extensions`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk). ## DOM alternatives Use the following UI extension alternatives instead of DOM APIs: | DOM API | Alternative | Purpose | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | | `document.querySelector()` | [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) | Build interface with React components | | `document.getElementById()` | [React refs](https://react.dev/reference/react/useRef) | Reference elements within components | | `document.body` / `document.title` | Component props and state | Manage UI through React state | | `element.innerHTML` | JSX and UI components | Render content declaratively | | `document.cookie` | [`hubspot.fetch()`](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#hubspot-fetch) + backend | Store data via backend APIs | See the next section for an example of each alternative. ## Examples ### Querying elements Instead of using `document.querySelector()` or `document.getElementById()`: ```jsx theme={null} // Incorrect const element = document.querySelector('.my-element'); const body = document.body; const title = window.document.title; const cookie = window['document'].cookie; ``` Build your UI with [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview): ```jsx theme={null} import { Text, Button, Flex, Heading } from '@hubspot/ui-extensions'; import { hubspot } from '@hubspot/ui-extensions'; import { useState } from 'react'; const Extension = () => { const [count, setCount] = useState(0); return ( Counter Example Count: {count} ); }; hubspot.extend(() => ); ``` ### Element references Instead of using `document.getElementById()` in effects: ```jsx theme={null} // Incorrect import { useEffect } from 'react'; function Component() { useEffect(() => { const element = document.getElementById('my-id'); element.focus(); }, []); return Component; } ``` Use [React refs](https://react.dev/reference/react/useRef) and component props: ```jsx theme={null} import { Input, Flex } from '@hubspot/ui-extensions'; import { hubspot } from '@hubspot/ui-extensions'; import { useState, useRef, useEffect } from 'react'; const Extension = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const nameInputRef = useRef(null); useEffect(() => { // Focus the input using ref if (nameInputRef.current) { nameInputRef.current.focus(); } }, []); return ( setName(value)} /> setEmail(value)} /> ); }; hubspot.extend(() => ); ``` ### Data fetching Instead of fetching data and manipulating the DOM: ```jsx theme={null} // Incorrect useEffect(() => { fetch('/api/user') .then(res => res.json()) .then(data => { document.getElementById('username').textContent = data.name; }); }, []); ``` Use [CRM data fetching hooks](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk#fetch-crm-property-data) and React state: ```jsx theme={null} import { Text, LoadingSpinner, Flex } from '@hubspot/ui-extensions'; import { hubspot, logger } from '@hubspot/ui-extensions'; import { useCrmProperties } from '@hubspot/ui-extensions/crm'; import { useState, useEffect } from 'react'; const Extension = () => { const { properties, isLoading, error } = useCrmProperties([ 'firstname', 'lastname', 'email', ]); const [displayName, setDisplayName] = useState(''); useEffect(() => { if (properties.firstname && properties.lastname) { const fullName = `${properties.firstname} ${properties.lastname}`; setDisplayName(fullName); logger.info(`Loaded contact: ${fullName}`); } }, [properties]); if (isLoading) return ; if (error) return Error loading data; return ( {displayName} {properties.email} ); }; hubspot.extend(() => ); ``` ## Related resources * [UI extensions SDK overview](/docs/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk) * [UI components](/docs/apps/developer-platform/add-features/ui-extensions/ui-components/overview) * [Fetching data](/docs/apps/developer-platform/add-features/ui-extensions/fetching-data) # no-html-elements Source: https://developers.hubspot.com/docs/apps/developer-platform/add-features/ui-extensions/tools/linting/rules/no-html-elements Disallow usage of unsupported HTML elements in UI extensions. The `no-html-elements` rule disallows usage of standard [HTML elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements) (`
`, ``, `