Skip to main content

Build custom component libraries

Learn how to build custom React components in Retool.

Custom components libraries is distinct from the legacy custom component feature. It is available on Retool Cloud and self-hosted Retool from version 3.41.0 and later. Retool does not recommend creating custom components using the legacy feature.

If Retool's built-in components don't work for your use case, you can build custom components in React. Custom components are written locally in your development environment and then deployed to Retool. After deploying, you can drag and drop components into apps as you would any other component.

To allow custom components to interact with your Retool app, Retool provides a TypeScript API. This API lets you define events that trigger event handlers and add properties to the Inspector for your component.

Custom components are contained within libraries, and each library has a unique name. Custom components deployed to Retool are automatically shown in the Component Library, in a section that matches the library's label.

Prerequisites

To build a custom component library, you need:

Write a custom component

The following steps guide you through the development flow to write and use a custom component. The example component contains a Name property and displays a message with the provided name. The code will look something like this:

export const HelloWorldComponent: FC = () => {
// Allows the builder to specify a "name" property on each component they build with.
// The builder can then pass data from their Retool app into the component by setting a value
// for the "name" property.
const [name, setName] = Retool.useStateString({
name: "name",
});

return (
<div>
<div>Hello {name}!</div>
</div>
);
};

After building the component, you can drag it to the canvas like any other component.

1. Clone the template repository

Clone the custom-component-collection-template repository using HTTPS or SSH.

git clone https://github.com/tryretool/custom-component-collection-template new-custom-component

2. Install dependencies

Change your directory to new-custom-component and run:

npm install

3. Log in to Retool

Log in using the following command. You need to provide an API access token with read and write scopes for Custom Component Libraries. See the Retool API authentication documentation for instructions on generating an access token.

npx retool-ccl login

4. Create a component library

Create a component library for your component.

npx retool-ccl init

This command guides you through picking a name and description for your library. It then adds those and other metadata to your local package.json file and calls Retool to construct the library.

5. Rename your component

Rename the HelloWorld component as needed. All components exported from src/index.tsx are synced to Retool.

6. Start dev mode

Use dev mode to test changes as you build your component:

npx retool-ccl dev

This syncs changes to Retool every time you change a file. You can then test component changes in any Retool app.

7. Add components to the canvas

Select the custom component from the Add components panel, and drag it into your app. The name of the custom component is the same as the name of your exported React component. The component is displayed in a section titled with the label of your library. You may need to refresh the page for new components to show up in the Add components panel.

8. Develop your component

Write code for your component in your preferred editor. See the TypeScript API section for more information about how to develop your component.

9. Deploy your component

When you're done creating your component, deploy it with the following command:

npx retool-ccl deploy

This pushes an immutable version of the component to Retool.

10. Switch component versions

To pin your app to the component version you just published, navigate to the Custom Component settings in your Retool app and change dev to the latest version. This may require you to refresh the page to see the newly published version.

Maintain your component

To make updates to your component, Retool recommends using dev mode to develop changes before publishing new versions. To do this, run npx retool-ccl dev as before, and then navigate to your app's Custom Component settings to change the version back to dev. When you're finished, follow the same steps to deploy your component and pin the app to the latest version.

To delete a library, sign in to your Retool organization and navigate to Settings > Custom Component Libraries.

Expand your library

You can add multiple custom components to your library by exporting more React components from src/index.tsx (only components exported from this file are detected). You can also create multiple libraries by creating a separate TypeScript project and following this guide again.

Pass data between your Retool app and your component

You can add properties to custom components that pass data to and from the Retool app using the TypeScript API.

export const HelloWorldComponent: FC = () => {
// Allows the builder to specify a "name" property on each component they build with.
// The builder can then pass data from their Retool app into the component by setting a value
// for the "name" property.
const [name, setName] = Retool.useStateString({
name: "name",
});

return (
<div>
<div>Hello {name}!</div>
</div>
);
};

The above component has a name property that any builder can then pass data into, like any other Retool component. For example, you can assign the value of name to be {{query[0].name}}, which sets name to be the result of a query in your Retool app. In the below example, {{query[0].name}} resolves to Daniel, which the custom component then displays.

Setting name to query[0].name

To pass data from the component back to your Retool app, you can use the setName method to set the value of name from within your custom component, and then reference this value in the rest of your Retool app using {{yourCustomComponentId.name}}.

TypeScript API

Custom components can interact with the Retool app they are installed to. They can fetch their state from Retool, set their default size when dragged onto the canvas, or tell Retool that one of their events has fired. There are separate TypeScript methods for each of these actions.

useState functions

These functions allow you to pass data from your Retool app into your custom component. Each function is for a different type of data.

Method signature
/**
* This method allows you to add boolean state to your component.
* Like any other component in Retool, custom components can have their own state, which you can then edit using the inspector.
*
* @param {string} name The name of the state used internally, and the label that will be used in the Inspector to identify it.
* This should be an alphanumerical string with no spaces.
* @param {boolean} [initialValue] The initial value for the state when the component is dragged onto the canvas.
* @param {('text' | 'checkbox' | 'hidden')} [inspector] What kind of Inspector will be used when a builder is editing this state.
* @param {string} [description] What will be displayed in the tooltip of the Inspector for this state.
* @param {string} [label] An override for the label used in the Inspector for this state.
*
* @return {[boolean, (newValue: boolean) => void]} The value of the state, and a function to update it.
*/
function useStateBoolean({
name,
initialValue,
inspector,
description,
label,
}: {
name: string
initialValue?: boolean
label?: string
description?: string
inspector?: 'text' | 'checkbox' | 'hidden'
}): readonly [boolean, (newValue: boolean) => void]

/**
* This method allows you to add number state to your component.
* Like any other component in Retool, custom components can have their own state, which you can then edit using the Inspector.
*
* @param {string} name The name of the state used internally, and the label that will be used in the Inspector to identify it.
* This should be an alphanumerical string with no spaces.
* @param {number} [initialValue] The initial value for the state when the component is dragged onto the canvas.
* @param {('text' | 'hidden')} [inspector] What kind of Inspector will be used when a builder is editing this state.
* @param {string} [description] What will be displayed in the tooltip of the Inspector for this state.
* @param {string} [label] An override for the label used in the Inspector for this state.
*
* @return {[number, (newValue: number) => void]} The value of the state, and a function to update it.
*/
function useStateNumber({
name,
initialValue,
inspector,
description,
label,
}: {
name: string
initialValue?: number
label?: string
description?: string
inspector?: 'text' | 'hidden'
}): readonly [number, (newValue: number) => void]

/**
* This method allows you to add string state to your component.
* Like any other component in Retool, custom components can have their own state, which you can then edit using the Inspector.
*
* @param {string} name The name of the state used internally, and the label that will be used in the Inspector to identify it.
* This should be an alphanumerical string with no spaces.
* @param {string} [initialValue] The initial value for the state when the component is dragged onto the canvas.
* @param {('text' | 'hidden')} [inspector] What kind of Inspector will be used when a builder is editing this state.
* @param {string} [description] What will be displayed in the tooltip of the Inspector for this state.
* @param {string} [label] An override for the label used in the Inspector for this state.
*
* @return {[string, (newValue: string) => void]} The value of the state, and a function to update it.
*/
function useStateString({
name,
initialValue,
inspector,
description,
label,
}: {
name: string
initialValue?: string
label?: string
description?: string
inspector?: 'text' | 'hidden'
}): readonly [string, (newValue: string) => void]

/**
* This method allows you to add enumeration state to your component. This is state that can have a value drawn from a limited set of strings.
* Like any other component in Retool, custom components can have their own state, which you can then edit using the Inspector.
*
* @param {string} name The name of the state used internally, and the label that will be used in the Inspector to identify it.
* This should be an alphanumerical string with no spaces.
* @param {T} enumDefinition An array of string literals describing the possible enum values. The strings must be alphanumeric with no spaces.
* @param {T[number]} [initialValue] The initial value for the state when the component is dragged onto the canvas.
* @param {{[K in T[number]]: string}} [enumLabels] Alternative labels to use for enums when displaying them.
* @param {('segmented' | 'select')} [inspector] What kind of Inspector will be used when a builder is editing this state.
* @param {string} [description] What will be displayed in the tooltip of the Inspector for this state.
* @param {string} [label] An override for the label used in the Inspector for this state.
*
* @return {[T[number], (newValue: T[number]) => void]} The value of the state, and a function to update it.
*/
function useStateEnumeration<T extends string[]>({
name,
enumDefinition,
initialValue,
enumLabels,
inspector,
description,
label,
}: {
name: string
initialValue?: T[number]
enumDefinition: T
enumLabels?: {
[K in T[number]]: string
}
inspector?: 'segmented' | 'select' | 'hidden'
description?: string
label?: string
}): readonly [T[number], (newValue: T[number]) => void]

/**
* This method allows you to add serializable object state to your component.
* Like any other component in Retool, custom components can have their own state, which you can then edit using the Inspector.
*
* @param {string} name The name of the state used internally, and the label that will be used in the Inspector to identify it.
* This should be an alphanumerical string with no spaces.
* @param {SerializableObject} [initialValue] The initial value for the state when the component is dragged onto the canvas.
* @param {('text' | 'hidden')} [inspector] What kind of Inspector will be used when a builder is editing this state.
* @param {string} [description] What will be displayed in the tooltip of the Inspector for this state.
* @param {string} [label] An override for the label used in the Inspector for this state.
*
* @return {[SerializableObject, (newValue: SerializableObject) => void]} The value of the state, and a function to update it.
*/
function useStateObject({
name,
initialValue,
inspector,
description,
label,
}: {
name: string
initialValue?: SerializableObject
inspector?: 'text' | 'hidden'
description?: string
label?: string
}): readonly [SerializableObject, (newValue: SerializableObject) => void]

/**
* This method allows you to add serializable array state to your component.
* Like any other component in Retool, custom components can have their own state, which you can then edit using the Inspector.
*
* @param {string} name The name of the state used internally, and the label that will be used in the Inspector to identify it.
* This should be an alphanumerical string with no spaces.
* @param {SerializableArray} [initialValue] The initial value for the state when the component is dragged onto the canvas.
* @param {('text' | 'hidden')} [inspector] What kind of Inspector will be used when a builder is editing this state.
* @param {string} [description] What will be displayed in the tooltip of the Inspector for this state.
* @param {string} [label] An override for the label used in the Inspector for this state.
*
* @return {[SerializableArray, (newValue: SerializableArray) => void]} The value of the state, and a function to update it.
*/
function useStateArray({
name,
initialValue,
inspector,
description,
label,
}: {
name: string
initialValue?: SerializableArray
inspector?: 'text' | 'hidden'
description?: string
label?: string
}): readonly [SerializableArray, (newValue: SerializableArray) => void]
Example
export const HelloWorldComponent: FC = () => {
// Allows the builder to specify a "name" property on each component they build with.

const [showBorder, _setShowBorder] = Retool.useStateBoolean({
name: "showBorder",
initialValue: false,
label: "Show Border",
inspector: "checkbox",
});

return (
<div
style={{
border: showBorder ? "1px solid black" : "",
}}
>
Hello!
</div>
);
};

Retool.useEventCallback

This method allows you to notify Retool of component events, which can then be used to trigger event handlers within Retool.

Method signature
/**
* Defines an event callback for your component. While building with the component you will be able to create event handlers that are triggered
* whenever this event callback is called.
*
* For example, you could create an event callback which is triggered whenever a user clicks on a button in your component.
*
* The event cannot contain any data or context.
*
* @param {string} name The name of the label that the event callback will be given in the Inspector.
*/
function useEventCallback({ name }: { name: string }): () => void

Example usage
export const ButtonComponent: FC = () => {
// Allows the custom component to inform Retool when a click event
// happens on the component. You can then add event handlers to any ButtonComponent
// that is triggered when onClick is called.
const onClick = Retool.useEventCallback({ name: "click" });

return (
<div>
<button onClick={onClick}> A button </button>
</div>
);
};

Passing data with an event

Data from a custom component can't be included inside an event. To access this kind of data in an event handler, set state in the component before firing the event (in this example text), and then access that state as you normally would in the event handler.

Accessing component data in an event handler
export const CustomInput: FC = () => {
const [text, setText] = Retool.useStateString({
name: "text",
inspector: "hidden",
});

const clickEvent = Retool.useEventCallback({ name: "click" });
return (
<>
<input
type="text"
id="name"
name="name"
value={text}
onChange={(change) => {
setText(change.target.value);
}}
/>
<button onClick={(event) => clickEvent()}>submit</button>
</>
);
};

In your event handler, you can then access the value of {{componentId.text}}.

Event handler that references the text property

Retool.useComponentSettings

Allows you to set the size of the component when it is first dragged onto the canvas.

Method signature
 /**
* Allows configuration of various settings on your component.
*
* @param {string} defaultWidth Sets the default width in columns of the component when you drag it onto the canvas.
* @param {string} defaultHeight Sets the default height in rows of the component when you drag it onto the canvas.
*/
function useComponentSettings({
defaultWidth,
defaultHeight,
}: {
defaultWidth?: number
defaultHeight?: number
}): void
Example usage
export const HelloWorldComponent: FC = () => {
// Sets the default height and width when HelloWorldComponent is
// dragged onto the canvas.
// dragged onto the canvas. Note that the rows are much shorter than the columns.
Retool.useComponentSettings({
defaultHeight: 50,
defaultWidth: 5,
});

return <div>Hello!</div>;
};

Dev mode

Retool recommends using dev mode when building a component. This updates your component in Retool as you make edits, which means you don't have to constantly deploy new versions and update your app in Retool.

Run the following command to use dev mode. The first time you run this command for a library it creates a dev version in Retool.

npx retool-ccl dev

Open Custom Component settings within your app, then set the version of your library to dev. Whenever you save your component files, they're automatically reflected in your Retool app. When you're done making changes, deploy a new version of the component and change your app to use that version instead of dev. This prevents someone else breaking your app by making changes to the component without you knowing.

Use custom component libraries across multiple instances or Spaces

If you deploy applications to multiple instances or to Retool Spaces, additional steps are required to use custom component libraries across them. When deploying the same custom component library to multiple instances or Spaces, choose a primary instance or Space for your components. This is the only instance or Space that you should ever log in or deploy your components to.

To use your components on other instances or Spaces, sync them from your primary instance or Space with the following command:

npx retool-ccl sync

This command guides you through configuring a sync target, and then copies over the library and all available versions. When new versions are available in the primary instance or Space, re-run the sync command to copy them to any others. Any applications that reference these components automatically use the recently synced version.

Limitations

There are some limitations on how custom component libraries can be used. If these limitations prevent you from using custom component libraries, you can use the legacy custom components feature.

  • Custom component libraries aren't yet supported when interacting with certain other Retool features, including:
    • Mobile
    • Public apps (although embedded apps are supported)
  • Custom component library descriptions cannot be edited.
  • Individual library revisions cannot be larger than 10MB, or 30MB in dev mode.
  • Custom component libraries only load JavaScript and CSS files at runtime. Any other file included in the revision bundle is ignored.

Examples

See the custom-component-examples GitHub repository for custom component examples.