> ## Documentation Index
> Fetch the complete documentation index at: https://developers.hubspot.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

---
id: afc597f2-f3bd-460d-9920-c83ea0cf4a61
---

# Hooks

> Reference information for hooks provided by the UI extensions SDK.

export const RequiredIndicator = () => {
  return <span className="required-indicator">
      required
    </span>;
};

Hooks are used to simplify accessing context, performing actions, and fetching CRM data within UI extensions. The hooks provided by the UI extensions SDK are optimized to prevent unnecessary re-renders and automatically clean up resources when components unmount. You can pass inline arrays and objects to the hooks directly, as memoization is not required.

## Universal hooks

<Card>
  **Universal hooks**

  Supported in all extension points: `crm.record.tab`, `crm.record.sidebar`, `crm.preview`, `helpdesk.sidebar`, `settings`, `home`

  | Hook                                          | Description                                                    |
  | --------------------------------------------- | -------------------------------------------------------------- |
  | [`useExtensionApi`](#useextensionapi)         | Access both context and actions from a single hook.            |
  | [`useExtensionContext`](#useextensioncontext) | Access contextual information about the extension environment. |
  | [`useExtensionActions`](#useextensionactions) | Access actions that can be performed within HubSpot.           |
  | [`useCrmSearch`](#usecrmsearch)               | Search CRM records by query or structured filters.             |
  | [`useDebounce`](#usedebounce)                 | Debounce a rapidly-changing value.                             |

  ```jsx wrap theme={null}
  import {
    useExtensionApi,
    // OR
    useExtensionContext,
    useExtensionActions,
    useCrmSearch,
    useDebounce
  } from "@hubspot/ui-extensions";
  ```
</Card>

### useExtensionApi

The `useExtensionApi` hook provides access to available actions and contextual information from a single hook, for extension components that need access to both actions and context.

Otherwise, it's best practice to use the more specific `useExtensionActions` or `useExtensionContext`, depending on your use case.

* For a complete list of available context properties, see the [context reference documentation](/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/context).
* For a complete list of available actions, see the [actions reference documentation](/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions). The actions available depend on the extension point location.

The following example uses the `useExtensionApi` hook to display an alert (action) containing the user's first name (context) when a button is clicked.

<Tabs>
  <Tab title="Basic usage">
    ```jsx theme={null}
    import { Button, hubspot, useExtensionApi } from '@hubspot/ui-extensions';

    hubspot.extend(() => <MyExtension />);

    function MyExtension() {
      const { actions, context } = useExtensionApi();

      return (
        <Button onClick={() => actions.addAlert({ message: `Hello ${context.user.firstName}!` })}>
          Click me
        </Button>
      );
    }
    ```
  </Tab>

  <Tab title="Typed usage">
    ```tsx theme={null}
    import { Button, hubspot, useExtensionApi } from '@hubspot/ui-extensions';

    hubspot.extend<'crm.record.tab'>(() => <MyExtension />);

    function MyExtension() {
      const { actions, context } = useExtensionApi<'crm.record.tab'>();

      return (
        <Button onClick={() => actions.addAlert({ message: `Hello ${context.user.firstName}!` })}>
          Click me
        </Button>
      );
    }
    ```
  </Tab>
</Tabs>

### useExtensionContext

The `useExtensionContext` hook provides access to contextual information about the current extension environment, including location and other relevant data.

For a complete list of available context properties, see the [context reference documentation](/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/context).

The following example accesses the current extension location and renders it in a `Text` component:

<Tabs>
  <Tab title="Basic usage">
    ```jsx theme={null}
    import { Text, hubspot, useExtensionContext } from '@hubspot/ui-extensions';

    hubspot.extend(() => <MyExtension />);

    function MyExtension() {
      const context = useExtensionContext();

      return (
        <Text>Current location: {context.location}</Text>
      );
    }
    ```
  </Tab>

  <Tab title="Typed usage">
    ```tsx theme={null}
    import { Text, hubspot, useExtensionContext } from '@hubspot/ui-extensions';

    hubspot.extend<'crm.record.tab'>(() => <MyExtension />);

    function MyExtension() {
      const context = useExtensionContext<'crm.record.tab'>();

      return (
        <Text>Current location: {context.location}</Text>
      );
    }
    ```
  </Tab>
</Tabs>

### useExtensionActions

The `useExtensionActions` hook provides access to various actions that can be performed within the HubSpot interface. It's a generic hook that can be typed with specific extension point locations for better TypeScript support.

For a complete list of available actions, see the [actions reference documentation](/apps/developer-platform/add-features/ui-extensions/ui-extensions-sdk/actions). The actions available depend on the extension point location.

The following example displays an alert when a button is clicked:

<Tabs>
  <Tab title="Basic usage">
    ```jsx theme={null}
    import { Button, hubspot, useExtensionActions } from '@hubspot/ui-extensions';

    hubspot.extend(() => <MyExtension />);

    function MyExtension() {
      const { addAlert } = useExtensionActions();

      return (
        <Button onClick={() => addAlert({ message: "Action completed!" })}>
          Click me
        </Button>
      );
    }
    ```
  </Tab>

  <Tab title="Typed usage">
    ```tsx theme={null}
    import { Button, hubspot, useExtensionActions } from '@hubspot/ui-extensions';

    hubspot.extend<'crm.record.tab'>(() => <MyExtension />);

    function MyExtension() {
      const { addAlert } = useExtensionActions<'crm.record.tab'>();

      return (
        <Button onClick={() => addAlert({ message: "Action completed!" })}>
          Click me
        </Button>
      );
    }
    ```
  </Tab>
</Tabs>

### useCrmSearch

The `useCrmSearch` hook searches CRM records of a given object type. The search is built using any combination of text query and structured filters, and returns record IDs, formatted properties, and optional associations.

<Tabs>
  <Tab title="Call">
    ```jsx wrap theme={null}
    useCrmSearch(
      {
        objectType: "contact",
        properties: ["firstname", "lastname", "email"],
        query: "john",
        filterGroups: [
          {
            filters: [
              { propertyName: "createdate", operator: "GT", value: "1704067200000" }
            ]
          }
        ],
        sorts: [{ propertyName: "createdate", direction: "DESCENDING" }],
        pageLength: 20,
      },
      {
        propertiesToFormat: "all",
        formattingOptions: {
          date: {
            format: "MM-DD-YYYY",
            relative: false,
          },
          dateTime: {
            format: "MM-DD-YYYY hh:mm",
            relative: false,
          },
          currency: {
            addSymbol: true,
          },
        },
      }
    );
    ```

    <ResponseField name="config" type="object" required>
      Configures the search request with the following parameters:

      * `objectType` <RequiredIndicator />: the object type to search. Accepts type IDs (`"0-1"`), names (`"contact"`, `"deal"`), or custom object names with a `p_` prefix (`"p_pets"`).
      * `properties`: an optional array of properties to return for each result.
      * `query`: an optional free-text search query matched against the object's default searchable properties.
      * `filterGroups`: an optional array of filter groups. Groups in the array are OR'd together; filters within a single group are AND'd. Each filter includes a `propertyName`, an `operator` (`EQ`, `NEQ`, `LT`, `LTE`, `GT`, `GTE`, `BETWEEN`, `IN`, `NOT_IN`, `HAS_PROPERTY`, `NOT_HAS_PROPERTY`, `CONTAINS_TOKEN`, `NOT_CONTAINS_TOKEN`), and a `value`, `values`, or `highValue` field depending on the operator. See [search the CRM](/api-reference/latest/crm/search-the-crm#filter-search-results) for more details.
      * `sorts`: an optional array of sort configurations. Each item requires a `propertyName` and a `direction` (`"ASCENDING"` or `"DESCENDING"`).
      * `pageLength`: an optional number of results per page. Default: `10`. Max: `200`.
    </ResponseField>

    <ResponseField name="propertiesToFormat" type="'all' | array">
      Either `'all'` or an array of property names to format.
    </ResponseField>

    <ResponseField name="formattingOptions" type="object">
      Contains formatting options for the values returned from date, datetime, and currency properties.

      * The `date` and `dateTime` objects can include `format` and `relative` subfields:
        * `format` (string): a date or datetime string like `MM-DD-YYYY` or `MM-DD-YYYY:mm:ss`. Supports [standard date time string formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
        * `relative` (boolean): set to `true` to display the amount of time passed since the returned value (e.g., `(1 day ago)` or `(1 hour ago)`).
      * The `currency` object can include `addSymbol` (boolean), which sets whether the currency symbol should display with the number. Set to `true` to display the currency symbol.
    </ResponseField>

    <Warning>
      **Please note:** formatting is applied based on property type (`date`, `datetime`, `currency`) rather than content. For example, `date` formatting will not apply to a string type property containing a date value.
    </Warning>
  </Tab>

  <Tab title="Response">
    ```jsx theme={null}
    {
      isLoading: false,
      error: null,
      results: [
        {
          "objectId": 123456,
          "properties": {
            "email": "john@example.com",
            "firstname": "John",
            "lastname": "Smith"
          }
        }
      ],
      total: 342,
      pagination: {
        "hasNextPage": true,
        "hasPreviousPage": false,
        "currentPage": 1,
        "pageSize": 10,
        "nextPage": () => {}, // function to fetch results for next page
        "previousPage": () => {}, // function to fetch results for previous page
        "reset": () => {} // function to reset to page one of results
      },
      isRefetching: false,
      refetch: () => {} // function to refetch the latest search results
    }
    ```

    <ResponseField name="isLoading" type="boolean">
      Indicates whether the data is being fetched.
    </ResponseField>

    <ResponseField name="error" type="object">
      For failed fetch requests, an object with error details. Will be `null` for successful requests.
    </ResponseField>

    <ResponseField name="results" type="array">
      The matching CRM records for the current page. Each item includes:

      * `objectId`: the ID of the CRM record.
      * `properties`: an object containing the requested property data, listed in alphabetical order.
    </ResponseField>

    <ResponseField name="total" type="number">
      The total number of matching records across all pages.
    </ResponseField>

    <ResponseField name="pagination" type="object">
      An object with pagination utilities, including:

      * `hasNextPage`: a boolean indicating if more pages are available.
      * `hasPreviousPage`: a boolean indicating if previous pages exist.
      * `currentPage`: the current page number.
      * `pageSize`: the number of items per page.
      * `nextPage()`: the function to go to the next page.
      * `previousPage()`: the function to go to the previous page.
      * `reset()`: the function to reset to the first page.
    </ResponseField>

    <ResponseField name="isRefetching" type="boolean">
      Indicates whether a refetch request is in progress.
    </ResponseField>

    <ResponseField name="refetch" type="function">
      A function to refetch the latest search results, with formatting applied as specified in the original hook call. The current page will be preserved.
    </ResponseField>
  </Tab>

  <Tab title="Example usage">
    ```jsx wrap highlight={10-22} theme={null}
    import {
      hubspot,
      Text,
      Button,
      Flex,
      useCrmSearch
    } from "@hubspot/ui-extensions";

    const Extension = () => {
      const { results, total, isLoading, error, pagination, isRefetching, refetch } = useCrmSearch(
        {
          objectType: 'contact',
          properties: ['firstname', 'lastname', 'email'],
          filterGroups: [{
            filters: [
              { propertyName: 'lifecyclestage', operator: 'EQ', value: 'lead' }
            ]
          }],
          sorts: [{ propertyName: 'createdate', direction: 'DESCENDING' }],
          pageLength: 10,
        }
      );

      if (isLoading) {
        return <Text>Loading results...</Text>;
      }

      if (isRefetching) {
        return <Text>Refetching results...</Text>;
      }

      if (error) {
        return <Text>Error loading results: {error.message}</Text>;
      }

      return (
        <Flex direction="column" gap="medium">
          <Text>{total} records found. Displaying page {pagination.currentPage}.</Text>

          {results.map(record => (
            <Flex direction="column" key={record.objectId}>
              <Text>
                {record.properties.firstname} {record.properties.lastname}: {record.properties.email}
              </Text>
            </Flex>
          ))}

          <Flex direction="row" gap="sm">
            <Button
              onClick={pagination.previousPage}
              disabled={!pagination.hasPreviousPage}
              variant="secondary"
            >
              Previous page
            </Button>
            <Button
              onClick={pagination.nextPage}
              disabled={!pagination.hasNextPage}
              variant="primary"
            >
              Next page
            </Button>
            <Button
              onClick={pagination.reset}
              variant="secondary"
            >
              Reset to first page
            </Button>
            <Button
              onClick={refetch}
              variant="primary"
            >
              Fetch latest data
            </Button>
          </Flex>
        </Flex>
      );
    };

    hubspot.extend(() => <Extension />);
    ```
  </Tab>

  <Tab title="Live search">
    ```jsx theme={null}
    import { useState } from 'react';
    import { hubspot, Input, Text, useCrmSearch, useDebounce } from '@hubspot/ui-extensions';

    const Extension = () => {
      const [query, setQuery] = useState('');
      // Waits for the user to stop typing before triggering a new search request
      const debouncedQuery = useDebounce(query);

      const { results, isLoading } = useCrmSearch({
        objectType: 'contact',
        properties: ['firstname', 'lastname', 'email'],
        query: debouncedQuery,
      });

      return (
        <>
          <Input
            value={query}
            onInput={setQuery}
            placeholder="Type to search..."
            label="Search Contacts"
            name="searchContacts"
          />
          {isLoading && <Text>Loading...</Text>}
          {results.map(record => (
            <Text key={record.objectId}>
              {record.properties.firstname} {record.properties.lastname}
            </Text>
          ))}
        </>
      );
    };

    hubspot.extend(() => <Extension />);
    ```
  </Tab>

  <Tab title="Filter by association">
    ```jsx theme={null}
    import React from 'react';
    import { hubspot, Text, useCrmSearch, useExtensionContext } from '@hubspot/ui-extensions';

    const Extension = () => {
      const { crm } = useExtensionContext();

      const { results, isLoading } = useCrmSearch(
        {
          objectType: 'deal',
          properties: ['dealname', 'amount', 'dealstage'],
          filterGroups: [
            {
              filters: [
                {
                  // 'associations.{objectType}' is a pseudo-property that filters by association
                  propertyName: 'associations.contact',
                  operator: 'EQ',
                  value: crm.objectId,
                },
              ],
            },
          ],
        },
        {
          propertiesToFormat: 'all',
          formattingOptions: {
            currency: {
              addSymbol: true,
            },
          },
        },
      );

      if (isLoading) return <Text>Loading associated deals...</Text>;

      return (
        <>
          {results.map((deal) => (
            <React.Fragment key={deal.objectId}>
              <Text>{deal.properties.dealname}</Text>
              <Text>Amount: {deal.properties.amount}</Text>
              <Text>Stage: {deal.properties.dealstage}</Text>
            </React.Fragment>
          ))}
        </>
      );
    };

    hubspot.extend(() => <Extension />);
    ```
  </Tab>
</Tabs>

### useDebounce

The `useDebounce` hook prevents a rapidly-changing value from propagating until it has stopped changing for a given number of milliseconds. Use it to avoid triggering expensive operations (like [CRM search API](/api-reference/latest/crm/search-the-crm) requests) on every change, and instead wait until the user has paused.

```jsx wrap theme={null}
import { useDebounce } from '@hubspot/ui-extensions';

const [searchText, setSearchText] = useState('');
// debouncedQuery only updates after searchText has been stable for 300ms
const debouncedQuery = useDebounce(searchText, 300);
```

The hook accepts any JSON-serializable value: strings, numbers, booleans, `null`, arrays, and objects. Objects and arrays are compared by deep equality, so a new object reference with the same content won't reset the debounce timer. Passing a non-serializable value (e.g., a function or a `Date` object) will result in incorrect deep equality comparisons.

```jsx wrap theme={null}
const [filters, setFilters] = useState({ status: 'OPEN', assignee: null });
const debouncedFilters = useDebounce(filters, 500);
```

| Parameter                     | Type                                                               | Description                                                                                                                                                                                                                                                                        |
| ----------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` <RequiredIndicator /> | `string` \| `number` \| `boolean` \| `null` \| `object` \| `array` | The value to debounce. Must be JSON-serializable. Objects and arrays are compared by deep equality, so a new object reference with the same content won't reset the debounce timer.<br /><br />Functions and class instances are not supported.                                    |
| `delayMs`                     | `number`                                                           | Milliseconds to wait after the last change before updating the debounced value. Defaults to `300`. Changing this value mid-render resets the pending timer. Passing a value of `0` still defers the update by one render cycle because the underlying `useEffect` is asynchronous. |

The hook returns the debounced version of `value` with the same type as the input. On the initial render, `value` is returned immediately without delay. The pending timer is cancelled when the component unmounts.

<Tabs>
  <Tab title="Debouncing a search input">
    Pair `useDebounce` with `useCrmSearch` (or any data-fetching hook) to avoid firing a request on every keystroke:

    ```jsx theme={null}
    import { useState } from 'react';
    import { Input, Text, useDebounce, useCrmSearch, hubspot } from '@hubspot/ui-extensions';

    const Extension = () => {
      const [query, setQuery] = useState('');
      const debouncedQuery = useDebounce(query, 300);

      const { results, isLoading } = useCrmSearch({
        objectType: '0-1',
        query: debouncedQuery,
        properties: ['firstname', 'lastname', 'email'],
      });

      return (
        <>
          <Input
            label="Search contacts"
            name="search-contacts"
            onInput={setQuery}
          />
          {isLoading && <Text>Searching...</Text>}
          {results.map((r) => (
            <Text key={r.objectId}>
              {r.properties.firstname} {r.properties.lastname}
            </Text>
          ))}
        </>
      );
    };

    hubspot.extend(() => <Extension />);

    ```
  </Tab>

  <Tab title="Showing validation after pause">
    Debounce validation to avoid showing an error message on every keystroke and instead waiting until the user has finished typing. In this example, the error only appears after the user pauses for 400ms.

    ```jsx theme={null}
    import { useState } from 'react';
    import { Input, Alert, useDebounce, hubspot } from '@hubspot/ui-extensions';

    const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    const Extension = () => {
      const [email, setEmail] = useState('');
      const debouncedEmail = useDebounce(email, 400);

      const isInvalid =
        debouncedEmail.length > 0 && !EMAIL_PATTERN.test(debouncedEmail);

      return (
        <>
          <Input label="Email address" name="email-address" onInput={setEmail} />
          {isInvalid && (
            <Alert variant="danger" title="Invalid email address">
              Don't forget to add a valid email address!
            </Alert>
          )}
        </>
      );
    };

    hubspot.extend(() => <Extension />);

    ```
  </Tab>
</Tabs>

## CRM-specific hooks

These hooks are only available in CRM extension points (`crm.record.tab`, `crm.record.sidebar`, `crm.preview`, and `helpdesk.sidebar`).

* [`useCrmProperties`](#usecrmproperties): fetch properties from the current CRM record
* [`useAssociations`](#useassociations): fetch associated CRM records

```jsx wrap theme={null}
import {
  useCrmProperties,
  useAssociations
} from "@hubspot/ui-extensions/crm";
```

### useCrmProperties

The `useCrmProperties` hook fetches properties from the current CRM record with optional formatting. It accepts an array of properties to fetch, along with an optional object to format the returned data.

<Tabs>
  <Tab title="Call">
    ```jsx theme={null}
    useCrmProperties(["firstname", "lastname", "email"], {
      propertiesToFormat: "all",
      formattingOptions: {
        date: {
          format: "MM-DD-YYYY",
          relative: false,
        },
        dateTime: {
          format: "MM-DD-YYYY hh:mm",
          relative: false,
        },
        currency: {
          addSymbol: true,
        },
      },
    });
    ```

    <ResponseField name="propertiesToFormat" type="'all' | array" required expandable={true} expanded={true}>
      Either `'all'` or an array of property names to format.
    </ResponseField>

    <ResponseField name="formattingOptions" type="object" expandable={true} expanded={true}>
      Contains formatting options for the values returned from date, datetime, and currency properties.

      * The `date` and `dateTime` objects can include `format` and `relative` subfields:
        * `format` (string): a date or datetime string like `MM-DD-YYYY` or `MM-DD-YYYY:mm:ss`. Supports [standard date time string formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
        * `relative` (boolean): set to `true` to display the amount of time passed since the returned value (e.g., `(1 day ago)` or `(1 hour ago)`).
      * The `currency` object can include `addSymbol` (boolean), which sets whether the currency symbol should display with the number. Set to `true` to display the currency symbol.
    </ResponseField>

    <Warning>
      Formatting is applied based on property type (`date`, `datetime`, `currency`) rather than content. For example, `date` formatting will not apply to a string type property containing a date value.
    </Warning>
  </Tab>

  <Tab title="Response">
    ```jsx theme={null}
    {
      isLoading: false,
      error: null,
      properties: {
        "firstname": "Joe",
        "lastname": "Smith",
        "email": "joe@joebiz.com"
      },
      isRefetching: false,
      refetch: () => {} // function to refetch properties
    }
    ```

    <ResponseField name="isLoading" type="boolean">
      Indicates whether the data is being fetched.
    </ResponseField>

    <ResponseField name="error" type="object">
      For failed fetch requests, an object with error details. Will be `null` for successful requests.
    </ResponseField>

    <ResponseField name="properties" type="object">
      An object with key-value pairs of returned properties, as formatted by `formattingOptions`. Properties are returned in alphabetical order.
    </ResponseField>

    <ResponseField name="isRefetching" type="boolean">
      Indicates whether a refetch request is in progress.
    </ResponseField>

    <ResponseField name="refetch" type="function">
      A function to refetch the latest property values, with formatting applied as specified in the original hook call.
    </ResponseField>
  </Tab>

  <Tab title="Example usage">
    ```jsx highlight={8-26} theme={null}
    import {
      hubspot,
      Text,
      Button
    } from "@hubspot/ui-extensions";
    import { useCrmProperties } from "@hubspot/ui-extensions/crm";

    const Extension = () => {
      const { properties, isLoading, error, refetch, isRefetching } = useCrmProperties(
        // Array of properties to return
        ['firstname', 'lastname', 'email'],
        // Optional formatting options for returned data
        {
          propertiesToFormat: 'all',
          formattingOptions: {
            date: {
              format: 'MM-DD-YYYY',
              relative: false
            },
            dateTime: {
              format: 'MM-DD-YYYY hh:mm',
              relative: false
            },
            currency: {
              addSymbol: true
            }
          }
        }
      );

      if (isLoading) {
        return <Text>Loading properties...</Text>;
      }

      if (isRefetching) {
        return <Text>Refetching properties...</Text>;
      }

      if (error) {
        return <Text>Error loading properties: {error.message}</Text>;
      }
      return (
        <>
          <Text>
            The contact is "{properties.firstname} {properties.lastname}"
            with email "{properties.email}".
          </Text>
          <Button onClick={refetch} variant="primary">
            Refetch property data
          </Button>
        </>
      );
    };
    hubspot.extend(() => <Extension />);
    ```
  </Tab>
</Tabs>

### useAssociations

The `useAssociations` hook fetches CRM records of a specific object type associated with the currently displaying record. It accepts an object containing configuration details for the association fetch request, and an optional object that formats returned property data.

<Tabs>
  <Tab title="Call">
    ```jsx theme={null}
    useAssociations(
      {
        // Object type ID to fetch associations for
        toObjectType: "0-1",
        // Optional properties to fetch from associated records
        properties: ["firstname", "lastname", "email", "phone"],
        // Optional pagination settings
        pageLength: 25,
      },
      // Optional formatting configuration (same as useCrmProperties)
      {
        propertiesToFormat: "all",
        formattingOptions: {
          date: {
            format: "MM-DD-YYYY",
            relative: false,
          },
          dateTime: {
            format: "MM-DD-YYYY hh:mm",
            relative: false,
          },
          currency: {
            addSymbol: true,
          },
        },
      }
    );
    ```

    <ResponseField name="config" type="object" required>
      Configures the association data fetch request with:

      * `toObjectType`: the object type ID to fetch associations from (e.g., '0-1' for contacts).
      * `properties`: an optional array of properties to fetch from associated records.
      * `pageLength`: an optional number of items per page (defaults to 10).
    </ResponseField>

    <ResponseField name="propertiesToFormat" type="'all' | array" required>
      Either `'all'` or an array of property names to format.
    </ResponseField>

    <ResponseField name="formattingOptions" type="object">
      Contains formatting options for the values returned from date, datetime, and currency properties.

      * The `date` and `dateTime` objects can include `format` and `relative` subfields:
        * `format` (string): a date or datetime string like `MM-DD-YYYY` or `MM-DD-YYYY:mm:ss`. Supports [standard date time string formats](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format).
        * `relative` (boolean): set to `true` to display the amount of time passed since the returned value (e.g., `(1 day ago)` or `(1 hour ago)`).
      * The `currency` object can include `addSymbol` (boolean), which sets whether the currency symbol should display with the number. Set to `true` to display the currency symbol.
    </ResponseField>
  </Tab>

  <Tab title="Response">
    ```jsx theme={null}
    {
      isLoading: false,
      error: null,
      results: [
        {
          "toObjectId": 70284463640,
          "associationTypes": [
              {
                  "category": "HUBSPOT_DEFINED",
                  "typeId": 449,
                  "label": ""
              }
          ],
          "properties": {
              "email": "emailmaria@hubspot.com",
              "firstname": "Maria",
              "lastname": "Johnson (Sample Contact)"
          }
        }
      ],
      pagination: {
        "hasNextPage": true,
        "hasPreviousPage": false,
        "currentPage": 1,
        "pageSize": 1,
        "nextPage": () => {}, // function to fetch results for next page
        "previousPage": () => {}, // function to fetch results for previous page
        "reset": () => {} // function to reset to page one of results
      },
      isRefetching: false,
      refetch: () => {} // function to refetch the latest associations data
    }
    ```

    <ResponseField name="isLoading" type="boolean">
      Indicates whether the data is being fetched.
    </ResponseField>

    <ResponseField name="error" type="object">
      For failed fetch requests, an object with error details. Will be `null` for successful requests.
    </ResponseField>

    <ResponseField name="results" type="array">
      Association details of the returned CRM records. Includes the following fields:

      * `toObjectId`: the ID of the CRM record.
      * `associationTypes`: an array of [association type](/api-reference/latest/crm/associations/associate-records/guide) information.
      * `properties`: an object containing the requested property data, listed in alphabetical order.
    </ResponseField>

    <ResponseField name="pagination" type="object">
      An object with pagination utilities, including:

      * `hasNextPage`: a boolean indicating if more pages are available.
      * `hasPreviousPage`: a boolean indicating if previous pages exist.
      * `currentPage`: the current page number.
      * `pageSize`: the number of items per page.
      * `nextPage()`: the function to go to next page.
      * `previousPage()`: the function to go to previous page.
      * `reset()`: the function to reset to first page.
    </ResponseField>

    <ResponseField name="isRefetching" type="boolean">
      Indicates whether a refetch request is in progress.
    </ResponseField>

    <ResponseField name="refetch" type="function">
      A function to refetch the latest associations data, with formatting applied as specified in the original hook call. The current page will be preserved.
    </ResponseField>
  </Tab>

  <Tab title="Example usage">
    ```jsx wrap highlight={10-37} theme={null}
    import {
      Text,
      Button,
      Flex,
      hubspot
    } from "@hubspot/ui-extensions";
    import { useAssociations } from "@hubspot/ui-extensions/crm";

    const Extension = () => {
      const { results, error, isLoading, pagination, isRefetching, refetch } = useAssociations(
        {
          // Object type ID to fetch associations for
          toObjectType: '0-1',
          // Optional properties to fetch from associated objects
          properties: ['firstname', 'lastname', 'email', 'phone'],
          // Optional pagination settings
          pageLength: 25,
        },
        // Optional formatting configuration (same as useCrmProperties)
        {
          propertiesToFormat: 'all',
          formattingOptions: {
            date: {
              format: 'MM-DD-YYYY',
              relative: false
            },
            dateTime: {
              format: 'MM-DD-YYYY hh:mm',
              relative: false
            },
            currency: {
              addSymbol: true
            }
          }
        }
      );

      if (isLoading) {
        return <Text>Loading associations...</Text>;
      }

      if (isRefetching) {
        return <Text>Refetching associations...</Text>;
      }

      if (error) {
        return <Text>Error loading associations: {error.message}</Text>;
      }

      return (
        <Flex direction="column" gap="medium">
          <Text>Associations (Page {pagination.currentPage})</Text>

          {results.map((association, index) => (
            <Flex direction="column" key={association.toObjectId}>
              <Text>Association {index + 1}: Object ID {association.toObjectId}</Text>
              <Text>
                Association Types: {association.associationTypes.map(type => type.label).join(', ')}
              </Text>
              {Object.entries(association.properties).map(([key, value]) => (
                <Text key={key}>{key}: {value || 'N/A'}</Text>
              ))}
            </Flex>
          ))}

          <Flex direction="row" gap="sm">
            <Button
              onClick={pagination.previousPage}
              disabled={!pagination.hasPreviousPage}
              variant="secondary"
            >
              Previous Page
            </Button>
            <Button
              onClick={pagination.nextPage}
              disabled={!pagination.hasNextPage}
              variant="primary"
            >
              Next Page
            </Button>
            <Button
              onClick={pagination.reset}
              variant="secondary"
            >
              Reset to First Page
            </Button>
            <Button
              onClick={refetch}
              variant="primary"
            >
              Fetch Latest Data
            </Button>
          </Flex>
        </Flex>
      );
    };

    hubspot.extend(() => <Extension />);

    ```
  </Tab>
</Tabs>

## Best practices

### Always use TypeScript generics

```tsx theme={null}
// ✅ Good - Typed for better IntelliSense and type safety
const actions = useExtensionActions<'crm.record.tab'>();
const context = useExtensionContext<'crm.record.tab'>();

// ❌ Avoid - Less type safety and IntelliSense
const actions = useExtensionActions();
const context = useExtensionContext();
```

### Extract hook calls to component level

```tsx theme={null}
// ✅ Good - Hooks at component level
function MyExtension() {
  const { addAlert } = useExtensionActions<'crm.record.tab'>();
  const context = useExtensionContext<'crm.record.tab'>();

  const handleClick = () => {
    addAlert({ message: `Action from ${context.location}` });
  };

  return <Button onClick={handleClick}>Click me</Button>;
}

// ❌ Avoid - Don't call hooks in event handlers
function MyExtension() {
  const handleClick = () => {
    const { addAlert } = useExtensionActions(); // Wrong!
    addAlert({ message: "Hello" });
  };

  return <Button onClick={handleClick}>Click me</Button>;
}
```
