Getting started with 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 fully released on Retool Cloud. On self-hosted Retool it's available in beta from version 3.41, and is fully released in 3.75.0. 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:
- Node.js v20 or later installed in your development environment.
- Admin permissions in Retool.
- [Optional] If you are running self-hosted Retool, setting the ALLOW_SAME_ORIGIN_OPTION, and SANDBOX_DOMAIN environment variables is recommended.
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.
- HTTPS
- SSH
git clone https://github.com/tryretool/custom-component-collection-template new-custom-component
git clone git@github.com:tryretool/custom-component-collection-template.git 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.
Now your component library is ready for production use. If you want to use your component library in public apps, then you need to go to Settings > Custom Component Libraries and set it to be public as well. This will expose the component library to anyone who has access to the public app URL.
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.
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 added 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.
/**
* 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]
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.
/**
* 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
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.
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}}
.
Retool.useComponentSettings
Allows you to set the size of the component when it is first dragged onto the canvas.
/**
* 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
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. Each developer (identified by the access key supplied at npx retool-ccl login
) has their own dev
revision of the component library.
Run the following command to use dev mode. The first time you run this command for a library it creates your dev
version in Retool.
npx retool-ccl dev
This command continues to run and watch for file changes. Whenever you save your component files, they're automatically reflected in your Retool app.
With your dev version created, you can now use it in your Retool app. In App settings > Custom Components, you can switch between your dev version dev: {your-email}
or any other published version. When you're ready to publish, be sure to change your app to use a published version instead of the dev version.
Debugging your components
While writing your custom components, inevitably you will run into issues that require debugging. To debug your components, Retool recommends running them in dev mode. In dev mode, Retool includes source maps for your components, so you can use your browser's step-by-step debugging tools to debug any issues that come up.
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.
Send custom HTTP headers or cookies
If you have a self-hosted Retool deployment, you can use npx retool-ccl
to send additional custom headers or cookies with each HTTP request. This can be helpful if there is an application load balancer that:
- Is between where you develop your custom components and the deployment instance.
- Blocks HTTP requests that are missing certain authentication headers or cookies.
To send custom HTTP headers or cookies, use the --header 'header-name: header_value'
option when using npx retool-ccl
.
Every command supports this option. You can also include it multiple times for each header to include.
npx retool-ccl login --header 'header-name: header_value' --header 'header-name-2: header_value_2'
When using the sync
command, include --target-header
to specify headers for the target instance of Retool. --header
sends headers to the origin instance of Retool.
npx retool-ccl sync --header 'origin-header-name: value_1' --target-header 'target-header-name: value_2'
You can also send multiple cookies using a header name of Cookie
.
npx retool-ccl deploy --header 'Cookie: cookie_one=cookie_value_one; cookie_two=cookie_value_two'
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 supported in Retool Mobile
- 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.
- Node.js v20 or later is required for your development environment.
- Admin permissions in Retool are required.
- Custom components must be written in React and Typescript.
- When you download a page in PDF format, custom components are not included.
Size limit
For Retool Cloud organizations, there is a 5GB limit on the total size of all custom component libraries within an organization. If you need to deploy new revisions and have reached this limit, you can free up space by visiting the Custom Components settings page and deleting some existing libraries.
Examples
See the custom-component-examples
GitHub repository for custom component examples.