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

How to Use State Variables in Retool

Temporary state variables in Retool are page-scoped variables that store UI state between user interactions. Create them in the State panel, read their value with {{ state1.value }}, and update them with await state1.setValue(newValue). Note that setValue() is asynchronous — do not read the value immediately after calling it without await.

What you'll learn

  • How to create temporary state variables and set initial values
  • How to update state with state1.setValue() and the async gotcha to avoid
  • How to update nested objects in state using state1.setIn()
  • How to bind state variable values to components with {{ state1.value }}
  • Practical patterns: toggle flags, accumulating form data, tracking UI state
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read10-15 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
TL;DR

Temporary state variables in Retool are page-scoped variables that store UI state between user interactions. Create them in the State panel, read their value with {{ state1.value }}, and update them with await state1.setValue(newValue). Note that setValue() is asynchronous — do not read the value immediately after calling it without await.

Quick facts about this guide
FactValue
ToolRetool
DifficultyBeginner
Time required10-15 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

Managing Page-Scoped State in Retool

Temporary state variables are the primary tool for managing UI state within a single Retool page. Unlike query results (which come from your data source) or component values (which come from user input), state variables are pure UI state — a counter, a toggle flag, the currently selected tab, or a partial form object being built up across multiple steps.

Retool state variables are session-scoped to their page. They reset to their default value whenever the page reloads or the user navigates away. This makes them perfect for transient UI state but not for data that needs to survive navigation.

This tutorial walks through creating state variables, using setValue() and setIn(), binding them to components, and common patterns like toggles and accumulated objects.

Prerequisites

  • A Retool account (Cloud or Self-hosted)
  • A Retool app with at least one page
  • Basic familiarity with JS Queries
  • Understanding of the Retool editor left sidebar panels

Step-by-step guide

1

Create a temporary state variable in the State panel

In the left sidebar of the Retool editor, click the State tab (it shows a cylinder/database-like icon). Click + New State Variable. Give it a name like isModalOpen, selectedTab, or filterState. Set a Default value — for booleans use false, for strings use an empty string, for objects use {}. The state variable will appear in the panel and is immediately accessible in the app as {{ stateVarName.value }}.

Expected result: A new state variable appears in the State panel. You can reference it in any component using {{ variableName.value }}.

2

Read the state variable value in a component

Bind the state variable's current value to any component using {{ variableName.value }}. For example, to show a loading spinner only when isLoading is true, set the Spinner component's Hidden property to {{ !isLoading.value }}. To display a counter in a Text component, set the Text value to {{ counter.value }}. For a Select component, bind the Value property to {{ selectedTab.value }}.

typescript
1// In the Text component's value field:
2{{ `Current filter: ${filterState.value}` }}
3
4// In a Button's Disabled property:
5{{ isLoading.value }}
6
7// In a Table's filter/search field:
8{{ searchTerm.value }}

Expected result: Components reflect the current state variable value and update reactively whenever setValue() is called.

3

Update state with setValue() in a JS Query

Create a JS Query and use await variableName.setValue(newValue) to update the state. The setValue() method is asynchronous — it returns a Promise. If you call setValue() without await and then immediately try to read variableName.value, you may get the old value. Always await the call when you need to use the new value in the same function.

typescript
1// JS Query: toggleModal
2// WRONG — do not read immediately without await
3isModalOpen.setValue(true);
4console.log(isModalOpen.value); // Still false!
5
6// CORRECT — await the Promise
7await isModalOpen.setValue(true);
8console.log(isModalOpen.value); // true
9
10// Toggle pattern
11await isModalOpen.setValue(!isModalOpen.value);

Expected result: The state variable updates correctly and components that reference {{ isModalOpen.value }} re-render.

4

Update nested objects with setIn()

When your state variable holds an object, use setIn() to update a specific nested key without overwriting the entire object. The signature is variableName.setIn(['path', 'to', 'key'], newValue). This is much safer than reading the full object, spreading it, and calling setValue() — setIn() is atomic.

typescript
1// State variable 'formData' with default value: {}
2// { name: '', email: '', address: { city: '', zip: '' } }
3
4// Update top-level key
5await formData.setIn(['name'], textInput1.value);
6
7// Update nested key
8await formData.setIn(['address', 'city'], cityInput.value);
9
10// Read the full updated object
11console.log(formData.value);
12// { name: 'Alice', email: '', address: { city: 'New York', zip: '' } }

Expected result: The nested key in the state variable updates without affecting other keys in the object.

5

Build a toggle button using state

A common pattern is a toggle button that switches between two states. Create a boolean state variable named showAdvanced with a default of false. Add a Button component with the label bound to {{ showAdvanced.value ? 'Hide advanced options' : 'Show advanced options' }}. In the Button's Click event handler, create a JS Query that toggles the value.

typescript
1// JS Query: toggleAdvanced (triggered by Button click)
2await showAdvanced.setValue(!showAdvanced.value);
3
4// Button label (in Inspector → General → Label):
5{{ showAdvanced.value ? 'Hide advanced options' : 'Show advanced options' }}
6
7// Advanced section Container → Hidden property:
8{{ !showAdvanced.value }}

Expected result: Clicking the button toggles the advanced section visibility, with the button label updating to reflect the current state.

6

Accumulate multi-step form data in state

Use a state variable as an accumulator when building multi-step forms on a single page (using a Tabbed Container or a Stepped Container). Initialize the state variable formData with default {}. On each step's Next button, merge the current step's inputs into the state object using setIn() or a spread + setValue() pattern. On the final step, read formData.value and pass it to your submission query.

typescript
1// Step 1 Next button — JS Query: saveStep1
2await formData.setIn(['firstName'], firstNameInput.value);
3await formData.setIn(['lastName'], lastNameInput.value);
4await formData.setIn(['email'], emailInput.value);
5// Advance to next tab
6await tabbedContainer1.setCurrentTab(1);
7
8// Step 2 Next button — JS Query: saveStep2
9await formData.setIn(['company'], companyInput.value);
10await formData.setIn(['role'], roleSelect.value);
11await tabbedContainer1.setCurrentTab(2);
12
13// Final Submit button — JS Query: submitForm
14const payload = formData.value;
15await createUserRecord.trigger({
16 additionalScope: { userData: payload }
17});
18await formData.setValue({}); // Reset after submit

Expected result: Form data accumulates across steps in the state variable and is submitted as a complete object on the final step.

Complete working example

JS Query: stateVariablePatterns
1// === Pattern 1: Simple toggle ===
2// State variable: isExpanded (default: false)
3await isExpanded.setValue(!isExpanded.value);
4
5// === Pattern 2: Accumulate object with setIn ===
6// State variable: formData (default: {})
7await formData.setIn(['name'], textInput1.value);
8await formData.setIn(['status'], statusSelect.value);
9await formData.setIn(['metadata', 'createdAt'], new Date().toISOString());
10
11// === Pattern 3: Array append ===
12// State variable: selectedIds (default: [])
13const currentIds = selectedIds.value || [];
14const newId = table1.selectedRow.data.id;
15if (!currentIds.includes(newId)) {
16 await selectedIds.setValue([...currentIds, newId]);
17}
18
19// === Pattern 4: Reset state on cancel ===
20await formData.setValue({});
21await selectedIds.setValue([]);
22await isExpanded.setValue(false);
23
24// === Pattern 5: Read after await (correct) ===
25await counter.setValue(counter.value + 1);
26console.log('New count:', counter.value); // Updated value

Common mistakes

Why it's a problem: Reading state1.value immediately after state1.setValue() without await

How to avoid: Change state1.setValue(val) to await state1.setValue(val). Without the await keyword, you are reading the stale value because setValue() is a Promise.

Why it's a problem: Using state variables in transformers and wondering why updates do not fire

How to avoid: Transformers are read-only — they cannot call setValue() or trigger queries. Move your update logic to a JS Query instead.

Why it's a problem: Overwriting an entire object state variable when only one key needs changing

How to avoid: Use setIn(['keyName'], newValue) instead of setValue({ ...stateVar.value, keyName: newValue }). setIn() is safer and more concise.

Why it's a problem: Expecting state variables to persist after page navigation

How to avoid: State variables are session-scoped to a single page. For data that needs to survive page navigation, use global variables (multipage apps) or URL parameters.

Best practices

  • Always await state1.setValue() before reading state1.value in the same function — setValue is asynchronous and the value is stale without await.
  • Use setIn() for nested object updates rather than spread + setValue() to avoid race conditions and accidental key overwriting.
  • Set meaningful default values when creating state variables — null for optional IDs, [] for arrays, {} for objects, false for booleans.
  • Name state variables clearly after what they represent, not how they work (e.g., isModalOpen not modalBooleanState).
  • Reset state variables in cleanup handlers (cancel button, successful submit) to prevent stale data from appearing in subsequent interactions.
  • Prefer state variables over component default values for dynamic defaults that change based on query data or user actions.
  • Remember that state variables reset on page reload — do not rely on them for data that must persist across sessions.

Still stuck?

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

ChatGPT Prompt

I am using Retool and need to manage UI state on a single page. I have a multi-step form inside a Tabbed Container and I want to accumulate form data across steps using a Temporary State variable. Show me how to create the state variable with a default of {}, use setIn() to update individual fields, and read the final accumulated object on the last step to send to a database query. Also explain the async setValue() gotcha.

Retool Prompt

In my Retool app, write a JS Query that: (1) reads the current value of a state variable named formData, (2) uses setIn() to update the 'email' key with textInput1.value, (3) awaits the setIn call, (4) then triggers a query named validateEmail. Make sure to handle the async nature of setValue/setIn correctly.

Frequently asked questions

What is the difference between a state variable and a temporary state in Retool?

They are the same thing. Retool uses 'temporary state' and 'state variable' interchangeably. Both refer to the variables you create in the State panel that are page-scoped, session-only, and accessible via variableName.value.

Can I use state variables inside a Retool transformer?

You can read a state variable's value inside a transformer using variableName.value. However, you cannot call setValue() or setIn() from within a transformer — transformers are read-only and cannot trigger state mutations or query executions. Use a JS Query for any writes.

How do I watch a state variable and run a query when it changes?

Create the query you want to run and set its Trigger method to 'Run when inputs change'. Then reference the state variable in the query's input fields using {{ stateName.value }}. Whenever setValue() updates the variable, the query will automatically re-run.

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.