# How to Create Custom Components in Retool

- Tool: Retool
- Difficulty: Intermediate
- Time required: 30-45 min
- Compatibility: Retool Cloud and Self-hosted (v3.0+)
- Last updated: March 2026

## TL;DR

Custom Component Libraries in Retool let you build React + TypeScript components that integrate natively with Retool's state, queries, and event system. Use the @tryretool/custom-component-support package and Retool hooks like Retool.useStateString() to read and write Retool state. Deploy with the Retool CLI to publish versioned, immutable releases your apps can reference.

## Build Production-Grade React Components for Retool

Retool's built-in component library covers most use cases, but sometimes you need a specialized UI — a rich text editor, a custom data visualization, a date-range picker with business logic baked in, or a third-party widget. Custom Component Libraries solve this by letting you write standard React + TypeScript components and deploy them to your Retool org.

Unlike the legacy iframe-based Custom Component (covered in the custom widgets tutorial), Custom Component Libraries use a proper npm package and CLI workflow. Your components can read and write Retool state via hooks like Retool.useStateString(), trigger queries with Retool.useEventCallback(), and expose typed properties in the Inspector panel — just like any native Retool component.

This tutorial walks through installing the CLI, scaffolding your first component, connecting it to Retool state, and publishing a versioned release.

## Before you start

- Node.js 18+ and npm installed on your local machine
- A Retool Cloud or self-hosted instance (v3.0 or later)
- Retool admin access to publish custom component libraries
- Basic familiarity with React and TypeScript
- A Retool API token (Settings → Retool API → Generate token)

## Step-by-step guide

### 1. Install the Retool Custom Component CLI

Open your terminal and create a new directory for your component library. Run `npx @tryretool/custom-component-support@latest init my-retool-components` to scaffold the project. The CLI will prompt you for your Retool instance URL and API token — enter your org URL (e.g., https://yourcompany.retool.com) and paste your API token. The scaffold creates a React + TypeScript project with Vite, the @tryretool/custom-component-support package pre-installed, and an example HelloWorld component.

```
npx @tryretool/custom-component-support@latest init my-retool-components
cd my-retool-components
npm install
```

**Expected result:** A new directory with package.json, src/index.tsx, and a configured retool.config.js file.

### 2. Examine the Retool hooks available in your component

Open src/index.tsx to see how the scaffold uses Retool hooks. The key hooks from @tryretool/custom-component-support are: Retool.useStateString(key, defaultValue) for string state, Retool.useStateNumber() for numbers, Retool.useStateBoolean() for booleans, Retool.useStateObject() for objects, and Retool.useEventCallback(eventName) to fire Retool event handlers. Each useStateXxx hook returns a [value, setValue] tuple — setValue() writes back to Retool's component Inspector model, making it visible to other components via {{ customComponent1.model.yourKey }}.

```
import Retool from "@tryretool/custom-component-support";

export const MyComponent: FC = () => {
  const [label, setLabel] = Retool.useStateString("label", "Hello");
  const [count, setCount] = Retool.useStateNumber("count", 0);
  const fireClick = Retool.useEventCallback("onClick");

  return (
    <button onClick={() => { setCount(count + 1); fireClick(); }}>
      {label}: {count}
    </button>
  );
};
```

**Expected result:** You understand the hook API and can see how component state maps to the Retool Inspector model.

### 3. Build a custom component that consumes query data

Replace the scaffold content in src/index.tsx with a component that accepts a data array from Retool. Define a useStateObject hook named 'rows' — in the Retool app, you'll bind this to {{ query1.data }}. In your component, map over the rows array to render custom HTML. This pattern lets you build tables, cards, or any layout that Retool's built-in Table component cannot express, while still pulling live data from Retool queries.

```
import Retool from "@tryretool/custom-component-support";
import React, { FC } from "react";

interface Row {
  id: number;
  name: string;
  status: "active" | "inactive";
}

export const StatusCardList: FC = () => {
  const [rows] = Retool.useStateObject("rows", []);
  const onSelect = Retool.useEventCallback("onRowSelect");

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      {(rows as Row[]).map((row) => (
        <div
          key={row.id}
          onClick={onSelect}
          style={{
            padding: 12,
            borderRadius: 6,
            background: row.status === "active" ? "#d1fae5" : "#fee2e2",
            cursor: "pointer",
          }}
        >
          <strong>{row.name}</strong> — {row.status}
        </div>
      ))}
    </div>
  );
};
```

**Expected result:** A React component file that accepts a rows array and fires an event when a card is clicked.

### 4. Register the component and run the development server

In src/index.tsx, ensure your component is the default export or is registered with defineComponent({ name: 'StatusCardList', component: StatusCardList }). Run `npm run dev` to start the local Vite dev server. The CLI automatically creates a temporary draft in your Retool org — open Retool in your browser and find the Custom Component Library in the component panel (it will show as 'Local Dev'). Drag it onto your canvas to test your component live with hot reloading.

```
// src/index.tsx — register your component
import { defineComponent } from "@tryretool/custom-component-support";
import { StatusCardList } from "./StatusCardList";

defineComponent({
  name: "StatusCardList",
  component: StatusCardList,
});
```

**Expected result:** Your component appears in Retool's canvas as a draggable Local Dev component with live reloading.

### 5. Bind Retool query data to the component's Inspector model

With the StatusCardList component on the canvas, click to select it and open the Inspector panel. You should see a 'rows' property under the Model section. Set its value to {{ query1.data }} — replace query1 with your actual query name. Now when query1 runs, its data array flows directly into your React component's useStateObject('rows') hook. You can also bind static arrays or use transformer data: {{ transformer1.value }}.

**Expected result:** The component displays live data from your Retool query. Cards appear with the correct status colors.

### 6. Wire up the onRowSelect event handler

In the Retool Inspector for your component, scroll to the Interaction section. You should see an 'onRowSelect' event listed (matching the name you passed to useEventCallback). Click '+ Add handler' and configure it to trigger a query or set a temporary state variable. For example, set a Temporary State variable selectedId to {{ customComponent1.model.id }} to capture which card was clicked. This bridges your custom component back into Retool's event/state system.

**Expected result:** Clicking a card fires the Retool event handler, and your selectedId state variable updates with the clicked row's ID.

### 7. Deploy an immutable versioned release

When development is complete, stop the dev server and run `npm run deploy` to publish your component library. The CLI will prompt for a version name (e.g., v1.0.0) and upload the built assets to your Retool org. Deployed versions are immutable — once published, a version's code never changes, giving your apps stable, reproducible builds. In Retool, switch from the 'Local Dev' version to the published version by opening the component's Library Version dropdown in the Inspector.

```
npm run deploy
```

**Expected result:** A versioned release appears in your Retool org's Custom Component Libraries settings page. Apps can now reference this stable version.

## Complete code example

File: `src/StatusCardList.tsx`

```typescript
import Retool from "@tryretool/custom-component-support";
import React, { FC, CSSProperties } from "react";

interface Row {
  id: number;
  name: string;
  status: "active" | "inactive" | "pending";
}

const statusStyles: Record<string, CSSProperties> = {
  active: { background: "#d1fae5", borderLeft: "4px solid #10b981" },
  inactive: { background: "#fee2e2", borderLeft: "4px solid #ef4444" },
  pending: { background: "#fef3c7", borderLeft: "4px solid #f59e0b" },
};

export const StatusCardList: FC = () => {
  const [rows] = Retool.useStateObject("rows", []);
  const [selectedId, setSelectedId] = Retool.useStateNumber("selectedId", -1);
  const onRowSelect = Retool.useEventCallback("onRowSelect");

  const handleClick = (row: Row) => {
    setSelectedId(row.id);
    onRowSelect();
  };

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8, padding: 4 }}>
      {(rows as Row[]).map((row) => (
        <div
          key={row.id}
          onClick={() => handleClick(row)}
          style={{
            padding: "10px 14px",
            borderRadius: 6,
            cursor: "pointer",
            fontFamily: "sans-serif",
            fontSize: 14,
            outline: row.id === selectedId ? "2px solid #6366f1" : "none",
            ...statusStyles[row.status],
          }}
        >
          <strong>{row.name}</strong>
          <span style={{ marginLeft: 8, opacity: 0.6 }}>{row.status}</span>
        </div>
      ))}
    </div>
  );
};
```

## Common mistakes

- **Reading a useStateXxx value immediately after calling setValue() and getting the stale value** — undefined Fix: setValue() is asynchronous — the updated value is not available in the same synchronous function call. Use a local variable to track the new value, or read it in a useEffect dependency.
- **Using the 'Local Dev' version in a published or production Retool app** — undefined Fix: Local Dev requires your laptop's dev server to be running. Always switch to a deployed versioned release before sharing or releasing an app.
- **Calling window.parent.postMessage() directly instead of using Retool hooks** — undefined Fix: Use Retool.useStateXxx() and Retool.useEventCallback() exclusively. Direct postMessage calls bypass Retool's model and may break in future versions.
- **Not registering the component with defineComponent(), causing it to be invisible in Retool** — undefined Fix: Every component must be registered with defineComponent({ name, component }) in your entry file (src/index.tsx).

## Best practices

- Always deploy immutable versioned releases to production — never use the 'Local Dev' version in production apps
- Keep component state minimal: store only data that Retool needs to reference via {{ customComponent1.model.key }}
- Use Retool.useEventCallback() for all outbound events — do not use window.postMessage() directly
- Add TypeScript interfaces for your model data to catch mismatches between Retool bindings and component expectations
- Test your component in both light and dark mode since Retool themes affect surrounding UI
- Bundle size matters: keep dependencies lean — large npm packages slow down component load in the Retool canvas
- Version semantically (v1.0.0, v1.1.0) and write release notes in the deploy prompt for team clarity

## Frequently asked questions

### Can I use any npm package in a Retool Custom Component Library?

Yes — your component is a standard Vite + React project, so you can install any npm package. However, large dependencies increase bundle size and slow down canvas loading. Prefer lightweight libraries, and check that the package doesn't rely on Node.js APIs (only browser APIs work in the Retool canvas).

### How do I pass data from my custom component back to a Retool query?

Use Retool.useStateXxx() hooks to write values into the component model (e.g., Retool.useStateString('outputValue', '')), then in the Retool app reference the value as {{ customComponent1.model.outputValue }}. You can also fire an event with useEventCallback() and trigger a query from the event handler.

### Do custom component libraries work in Retool Mobile?

Custom Component Libraries built with the CLI are designed for the Retool web editor. Retool Mobile uses its own React Native-based component system. For mobile, use the legacy Custom Component (iframe) approach or request native mobile component support via the Retool roadmap.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-create-custom-components-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-create-custom-components-in-retool
