Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Create Custom Components in Retool

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.

What you'll learn

  • Set up the Retool Custom Component Library CLI and scaffold a React project
  • Use Retool.useStateString(), Retool.useStateNumber(), and Retool.useStateObject() hooks
  • Pass data from Retool queries into your component via model properties
  • Trigger Retool queries from inside a custom component using Retool.useEventCallback()
  • Deploy immutable versioned releases to your Retool organization
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read30-45 minRetool Cloud and Self-hosted (v3.0+)Last updated March 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyIntermediate
Time required30-45 min
CompatibilityRetool Cloud and Self-hosted (v3.0+)
Last updatedMarch 2026

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.

Prerequisites

  • 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.

typescript
1npx @tryretool/custom-component-support@latest init my-retool-components
2cd my-retool-components
3npm 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 }}.

typescript
1import Retool from "@tryretool/custom-component-support";
2
3export const MyComponent: FC = () => {
4 const [label, setLabel] = Retool.useStateString("label", "Hello");
5 const [count, setCount] = Retool.useStateNumber("count", 0);
6 const fireClick = Retool.useEventCallback("onClick");
7
8 return (
9 <button onClick={() => { setCount(count + 1); fireClick(); }}>
10 {label}: {count}
11 </button>
12 );
13};

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.

typescript
1import Retool from "@tryretool/custom-component-support";
2import React, { FC } from "react";
3
4interface Row {
5 id: number;
6 name: string;
7 status: "active" | "inactive";
8}
9
10export const StatusCardList: FC = () => {
11 const [rows] = Retool.useStateObject("rows", []);
12 const onSelect = Retool.useEventCallback("onRowSelect");
13
14 return (
15 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
16 {(rows as Row[]).map((row) => (
17 <div
18 key={row.id}
19 onClick={onSelect}
20 style={{
21 padding: 12,
22 borderRadius: 6,
23 background: row.status === "active" ? "#d1fae5" : "#fee2e2",
24 cursor: "pointer",
25 }}
26 >
27 <strong>{row.name}</strong> {row.status}
28 </div>
29 ))}
30 </div>
31 );
32};

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.

typescript
1// src/index.tsx — register your component
2import { defineComponent } from "@tryretool/custom-component-support";
3import { StatusCardList } from "./StatusCardList";
4
5defineComponent({
6 name: "StatusCardList",
7 component: StatusCardList,
8});

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.

typescript
1npm 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 working example

src/StatusCardList.tsx
1import Retool from "@tryretool/custom-component-support";
2import React, { FC, CSSProperties } from "react";
3
4interface Row {
5 id: number;
6 name: string;
7 status: "active" | "inactive" | "pending";
8}
9
10const statusStyles: Record<string, CSSProperties> = {
11 active: { background: "#d1fae5", borderLeft: "4px solid #10b981" },
12 inactive: { background: "#fee2e2", borderLeft: "4px solid #ef4444" },
13 pending: { background: "#fef3c7", borderLeft: "4px solid #f59e0b" },
14};
15
16export const StatusCardList: FC = () => {
17 const [rows] = Retool.useStateObject("rows", []);
18 const [selectedId, setSelectedId] = Retool.useStateNumber("selectedId", -1);
19 const onRowSelect = Retool.useEventCallback("onRowSelect");
20
21 const handleClick = (row: Row) => {
22 setSelectedId(row.id);
23 onRowSelect();
24 };
25
26 return (
27 <div style={{ display: "flex", flexDirection: "column", gap: 8, padding: 4 }}>
28 {(rows as Row[]).map((row) => (
29 <div
30 key={row.id}
31 onClick={() => handleClick(row)}
32 style={{
33 padding: "10px 14px",
34 borderRadius: 6,
35 cursor: "pointer",
36 fontFamily: "sans-serif",
37 fontSize: 14,
38 outline: row.id === selectedId ? "2px solid #6366f1" : "none",
39 ...statusStyles[row.status],
40 }}
41 >
42 <strong>{row.name}</strong>
43 <span style={{ marginLeft: 8, opacity: 0.6 }}>{row.status}</span>
44 </div>
45 ))}
46 </div>
47 );
48};

Common mistakes when creating Custom Components in Retool

Why it's a problem: Reading a useStateXxx value immediately after calling setValue() and getting the stale value

How to avoid: 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.

Why it's a problem: Using the 'Local Dev' version in a published or production Retool app

How to avoid: Local Dev requires your laptop's dev server to be running. Always switch to a deployed versioned release before sharing or releasing an app.

Why it's a problem: Calling window.parent.postMessage() directly instead of using Retool hooks

How to avoid: Use Retool.useStateXxx() and Retool.useEventCallback() exclusively. Direct postMessage calls bypass Retool's model and may break in future versions.

Why it's a problem: Not registering the component with defineComponent(), causing it to be invisible in Retool

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a Custom Component Library for Retool using React and TypeScript. I need a component that: (1) accepts an array of objects from Retool via Retool.useStateObject('rows', []), (2) renders a styled card list based on a 'status' field, (3) fires a Retool event when a card is clicked using Retool.useEventCallback('onRowSelect'), (4) stores the selected row's ID back to Retool via Retool.useStateNumber('selectedId', -1). Please write the full TSX component and explain how to bind {{ query1.data }} to the 'rows' model property in the Retool Inspector.

Retool Prompt

Create a Retool Custom Component Library component that displays a status card list. The component should accept a 'rows' model property bound to {{ query1.data }}, display cards with color-coded status indicators using inline styles, and fire an 'onRowSelect' event when a card is clicked. Use Retool.useStateObject for the rows and Retool.useStateNumber to store the selectedId.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.