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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Intermediate |
| Time required | 30-45 min |
| Compatibility | Retool Cloud and Self-hosted (v3.0+) |
| Last updated | March 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
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.
1npx @tryretool/custom-component-support@latest init my-retool-components2cd my-retool-components3npm installExpected result: A new directory with package.json, src/index.tsx, and a configured retool.config.js file.
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 }}.
1import Retool from "@tryretool/custom-component-support";23export 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");78 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.
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.
1import Retool from "@tryretool/custom-component-support";2import React, { FC } from "react";34interface Row {5 id: number;6 name: string;7 status: "active" | "inactive";8}910export const StatusCardList: FC = () => {11 const [rows] = Retool.useStateObject("rows", []);12 const onSelect = Retool.useEventCallback("onRowSelect");1314 return (15 <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>16 {(rows as Row[]).map((row) => (17 <div18 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.
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.
1// src/index.tsx — register your component2import { defineComponent } from "@tryretool/custom-component-support";3import { StatusCardList } from "./StatusCardList";45defineComponent({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.
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.
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.
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.
1npm run deployExpected 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
1import Retool from "@tryretool/custom-component-support";2import React, { FC, CSSProperties } from "react";34interface Row {5 id: number;6 name: string;7 status: "active" | "inactive" | "pending";8}910const 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};1516export const StatusCardList: FC = () => {17 const [rows] = Retool.useStateObject("rows", []);18 const [selectedId, setSelectedId] = Retool.useStateNumber("selectedId", -1);19 const onRowSelect = Retool.useEventCallback("onRowSelect");2021 const handleClick = (row: Row) => {22 setSelectedId(row.id);23 onRowSelect();24 };2526 return (27 <div style={{ display: "flex", flexDirection: "column", gap: 8, padding: 4 }}>28 {(rows as Row[]).map((row) => (29 <div30 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation