Pass data between Retool components by directly referencing them in {{ }} expressions: {{ table1.selectedRow.data.id }} in a Text Input's Default Value. For cross-page data, use URL parameters with utils.openUrl() and read with {{ url.searchParams.key }}. For complex multi-component flows, use a Temporary State variable as an intermediary.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 15-20 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Data Flow Patterns Between Retool Components
In Retool, all named components in an app are globally accessible by name in {{ }} expressions. This means any component can read another component's value directly — a Text Input can default to {{ table1.selectedRow.data.name }}, a query can filter by {{ select1.value }}, and a Button can be disabled based on {{ form1.invalid }}. This direct-reference pattern is the primary mechanism for passing data between components on the same page.
For cross-page data passing, Retool uses URL parameters. A navigation event opens a new page URL with query string parameters, and the destination page reads them with {{ url.searchParams.paramName }}.
For complex flows where multiple components need to share and update the same value, a Temporary State variable acts as a shared store. Any component can read it; any JS Query can update it with setValue().
This tutorial covers all three patterns plus module inputs/outputs for Retool Modules, with guidance on when each pattern is appropriate.
Prerequisites
- Basic familiarity with adding components and the Inspector panel
- Understanding of {{ }} expression syntax
- For multi-page examples: a Retool app with at least two pages
Step-by-step guide
Pass data with direct component references
The simplest pattern: reference a component's value directly in another component's property using {{ componentName.property }}. This creates a live reactive binding — when the source component's value changes, all expressions referencing it update automatically. Common examples: binding a form's default to a table's selected row, filtering a query by a dropdown selection, or disabling a button based on a text input's length.
1// Text Input Default Value — pre-fill from selected table row:2{{ table1.selectedRow.data.customer_name }}34// SQL query WHERE clause — filter by dropdown:5SELECT * FROM orders WHERE customer_id = {{ customerSelect.value }}67// Button Disabled property — prevent action on empty input:8{{ searchInput.value.trim() === '' }}910// Text component content — display form summary:11{{ `Submitting: ${nameInput.value} (${emailInput.value})` }}1213// Number component value — display running total:14{{ (quantityInput.value || 0) * (priceInput.value || 0) }}Expected result: Referenced component values update downstream components in real time as the user interacts.
Pass data between components using a query's event chain
When component A's value should trigger component B to update based on a query result (not a direct reference), use an event handler chain. Example: user selects a customer in customerSelect → triggers getCustomerDetails query → a Text component displays {{ getCustomerDetails.data[0].phone }}. The component B is not directly bound to component A — it reads from the intermediate query result. This pattern is necessary when the relationship requires a database lookup.
1// customerSelect Change event handler:2// Event: Change → Trigger query → getCustomerDetails34// SQL query: getCustomerDetails5SELECT id, full_name, email, phone, address6FROM customers7WHERE id = {{ customerSelect.value }}8LIMIT 1;910// phoneText component content:11{{ getCustomerDetails.data[0]?.phone || 'Select a customer' }}1213// emailText component content:14{{ getCustomerDetails.data[0]?.email || '' }}Expected result: Selecting a customer triggers the details query and phone/email text components update with the result.
Pass data between pages with URL parameters
To send data from one page to another, add URL parameters when navigating. Use the 'Open URL' action in an event handler, or call utils.openUrl() from a JS Query. Build the URL with the data as query string parameters. On the destination page, read parameters with {{ url.searchParams.paramName }}.
1// Event handler on a Table row click (Inspector → Interaction):2// Action: Open URL3// URL: /retool/apps/my-app/edit-customer?id={{ table1.selectedRow.data.id }}&name={{ table1.selectedRow.data.full_name }}45// OR in a JS Query:6const customerId = table1.selectedRow.data.id;7const customerName = table1.selectedRow.data.full_name;8utils.openUrl(9 `/retool/apps/my-app/edit-customer?id=${customerId}&name=${encodeURIComponent(customerName)}`,10 { newTab: false }11);1213// On the destination page — read URL params:14// Text Input Default Value:15{{ url.searchParams.id }}1617// Text component showing customer name:18{{ url.searchParams.name }}1920// Query parameter using URL param:21SELECT * FROM customers WHERE id = {{ url.searchParams.id }}Expected result: Clicking a table row navigates to the edit page with customer data pre-loaded from the URL parameter.
Use Temporary State as a shared intermediary
When multiple components need to share a computed or user-selected value that does not map directly to a single component's property, use a Temporary State variable. Create it in the State panel, set a default value, then read it with {{ myState.value }} in any component. Update it from a JS Query with await myState.setValue(newValue). The state variable acts as a single source of truth that any component can read and any JS Query can update.
1// Temporary State: selectedCustomerId (default: null)2// Temporary State: cartItems (default: [])34// JS Query: selectCustomer (triggered from table row click)5await selectedCustomerId.setValue(table1.selectedRow.data.id);67// JS Query: addToCart (triggered from Add button click)8const current = cartItems.value;9const item = productsTable.selectedRow.data;10await cartItems.setValue([...current, item]);1112// SQL query using state variable:13SELECT * FROM orders WHERE customer_id = {{ selectedCustomerId.value }}1415// Cart count Text component:16{{ cartItems.value.length }} items1718// Cart total Text component:19{{ cartItems.value.reduce((sum, item) => sum + item.price, 0) }}Expected result: selectedCustomerId.value updates when JS Query runs. All components referencing {{ selectedCustomerId.value }} update reactively.
Pass data into a Retool Module using module inputs
Retool Modules are reusable app components. They expose declared inputs that consuming apps can pass values into. If you are using a Module in your app, select the Module component and in the Inspector, set each input value to a {{ }} expression. For example, if the module declares an input named userId, bind it to {{ currentUser.sid }} or {{ table1.selectedRow.data.id }}. The module receives these as module.inputs.userId internally.
1// In the consuming app — Module component Inspector inputs:2// userId: {{ table1.selectedRow.data.id }}3// apiKey: {{ secretInput.value }}4// dateRange: {{ dateRangePicker1.value }}56// Inside the Module (how it reads inputs):7// SQL query parameter: {{ module.inputs.userId }}8// JS Query: const userId = module.inputs.userId;910// Module output — read from the consuming app:11{{ moduleComponent1.outputs.totalRevenue }}12{{ moduleComponent1.outputs.recordCount }}Expected result: The Module component receives and uses the passed input values. Module outputs are accessible in the consuming app.
Choose the right pattern for your use case
Each data passing pattern has a specific use case. Direct {{ }} references are best for reactive bindings within a page where components stay in sync automatically. URL parameters are best for cross-page navigation where the destination page needs to know about a record to display. Temporary State is best for values that are computed or accumulated over multiple user interactions and need to be shared across multiple components. Module inputs/outputs are best for reusable component bundles shared across apps.
1// Decision guide:23// Use direct reference when:4// - Component B shows data from component A's current value5// - The relationship is 1-to-1 and synchronous6// Example: textInput1.value → SQL WHERE clause78// Use URL parameter when:9// - Navigating between pages and need to carry context10// - Deep linking from external systems11// Example: /app/details?orderId=421213// Use Temporary State when:14// - Multiple components contribute to a shared value15// - Value needs to persist across user actions (cart, selection list)16// - Complex computation result needs to be stored17// Example: selectedItems[] accumulates across multiple clicks1819// Use module inputs/outputs when:20// - Building a reusable component for multiple apps21// - Encapsulating a complex sub-featureExpected result: You can identify which pattern to use for any given data-passing scenario.
Complete working example
1// navigateToDetailPage2// Triggered by: table1 row click event handler3// Passes selected row data as URL parameters to the detail page45const row = table1.selectedRow.data;67if (!row || !row.id) {8 utils.showNotification({9 title: 'No row selected',10 description: 'Click a row in the table to open its detail view.',11 notificationType: 'warning',12 });13 return;14}1516// Build URL with relevant parameters17const params = new URLSearchParams({18 id: row.id,19 name: row.customer_name,20 status: row.status,21});2223// Navigate to the detail page24utils.openUrl(25 `/retool/apps/customer-detail?${params.toString()}`,26 { newTab: false } // set true to open in new tab27);2829// --- On the detail page ---30// Read URL params:31// Default Value of nameInput: {{ url.searchParams.name }}32// SQL query parameter: WHERE id = {{ url.searchParams.id }}33// Display status: {{ url.searchParams.status }}Common mistakes
Why it's a problem: Awaiting setValue() and reading the state variable on the next line — setValue() is async; the new value may not be reflected immediately
How to avoid: Use the value you just passed to setValue() rather than re-reading the state variable: await myState.setValue(newVal); doSomethingWith(newVal); not with myState.value.
Why it's a problem: Creating a circular reference: component A's value depends on component B's value, and component B's query is triggered by component A's Change event — causes infinite loops
How to avoid: Break the cycle by using a Temporary State variable as an intermediary. A Change event updates the state variable; B reads from the state variable; the dependency is one-directional.
Why it's a problem: Trying to pass data to a different browser tab using Temporary State — state variables are page-scoped and not shared between tabs
How to avoid: For cross-tab or cross-page data, use URL parameters or store the data in the database and fetch it on the destination page.
Why it's a problem: Using the component Label in a {{ }} expression instead of the component Name — labels can contain spaces and special characters, and are not valid expression identifiers
How to avoid: Always use the component's Name property (shown at the top of the Inspector, usually camelCase like customerSelect) in expressions, not the displayed Label.
Best practices
- Prefer direct {{ }} references over Temporary State for simple 1-to-1 reactive bindings — fewer moving parts, easier to debug
- Use encodeURIComponent() when building URL parameters with user-generated string values to handle special characters
- Give Temporary State variables meaningful names (selectedCustomerId, cartItems) rather than generic names (state1, temp) — they appear in {{ }} expressions throughout the app
- When chaining multiple components through query events, draw a dependency diagram first — circular references cause infinite re-query loops
- For Module inputs, declare all inputs explicitly in the Module editor — undeclared inputs are not accessible via module.inputs
- Avoid storing large arrays (thousands of items) in Temporary State — performance degrades; use query filters instead
- Document cross-page URL parameter contracts in the app description so other developers know what params each page accepts
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Retool app with two pages: a Customer List page and a Customer Detail page. On the List page, I have a table1 showing customers. When a user clicks a row, I want to navigate to the Detail page and pre-fill an edit form with that customer's data. Show me: (1) a JS Query on the List page that calls utils.openUrl() with the customer ID and name as URL parameters, (2) on the Detail page, how to read those URL params with {{ url.searchParams.id }} and use them in a SQL query and a form Default Value, and (3) when I should use Temporary State vs URL parameters for this kind of data passing.
In my Retool app, I want to pass the selected row from table1 to a form for editing. The table has columns: id, customer_name, email, status. I need the edit form's Text Inputs (nameInput, emailInput) and Select (statusSelect) to pre-fill with the selected row's values. What expressions do I put in the Default Value fields of each input? Should I use direct references or form.setData()?
Frequently asked questions
Can I pass data between two different Retool apps (not just pages within one app)?
Yes, via URL parameters. Use utils.openUrl() to navigate to the second app's URL with data in the query string. The receiving app reads them with {{ url.searchParams.key }}. You can also use the database as a shared data store — App A writes a record, App B reads it.
What is the maximum amount of data I can pass via URL parameters?
URL length limits vary by browser (typically 2,000-8,000 characters). For small identifiers (IDs, status strings), URL parameters work well. For complex objects or large datasets, store the data in the database and pass only the ID via URL. The destination page fetches the full record using that ID.
How do I pass an array of selected table rows (multi-select) to another component?
Access {{ table1.selectedRows }} (note the plural — this returns an array when multi-row selection is enabled). Store it in a Temporary State variable with await selectedRows.setValue(table1.selectedRows), then reference {{ selectedRows.value }} in downstream components. For URL navigation, pass only the IDs: table1.selectedRows.map(r => r.id).join(',').
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation