# How to Use State Variables in Retool

- Tool: Retool
- Difficulty: Beginner
- Time required: 10-15 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## 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.

## 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.

## Before you start

- 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 }}.

```
// In the Text component's value field:
{{ `Current filter: ${filterState.value}` }}

// In a Button's Disabled property:
{{ isLoading.value }}

// In a Table's filter/search field:
{{ 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.

```
// JS Query: toggleModal
// WRONG — do not read immediately without await
isModalOpen.setValue(true);
console.log(isModalOpen.value); // Still false!

// CORRECT — await the Promise
await isModalOpen.setValue(true);
console.log(isModalOpen.value); // true

// Toggle pattern
await 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.

```
// State variable 'formData' with default value: {}
// { name: '', email: '', address: { city: '', zip: '' } }

// Update top-level key
await formData.setIn(['name'], textInput1.value);

// Update nested key
await formData.setIn(['address', 'city'], cityInput.value);

// Read the full updated object
console.log(formData.value);
// { 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.

```
// JS Query: toggleAdvanced (triggered by Button click)
await showAdvanced.setValue(!showAdvanced.value);

// Button label (in Inspector → General → Label):
{{ showAdvanced.value ? 'Hide advanced options' : 'Show advanced options' }}

// Advanced section Container → Hidden property:
{{ !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.

```
// Step 1 Next button — JS Query: saveStep1
await formData.setIn(['firstName'], firstNameInput.value);
await formData.setIn(['lastName'], lastNameInput.value);
await formData.setIn(['email'], emailInput.value);
// Advance to next tab
await tabbedContainer1.setCurrentTab(1);

// Step 2 Next button — JS Query: saveStep2
await formData.setIn(['company'], companyInput.value);
await formData.setIn(['role'], roleSelect.value);
await tabbedContainer1.setCurrentTab(2);

// Final Submit button — JS Query: submitForm
const payload = formData.value;
await createUserRecord.trigger({
  additionalScope: { userData: payload }
});
await 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 code example

File: `JS Query: stateVariablePatterns`

```javascript
// === Pattern 1: Simple toggle ===
// State variable: isExpanded (default: false)
await isExpanded.setValue(!isExpanded.value);

// === Pattern 2: Accumulate object with setIn ===
// State variable: formData (default: {})
await formData.setIn(['name'], textInput1.value);
await formData.setIn(['status'], statusSelect.value);
await formData.setIn(['metadata', 'createdAt'], new Date().toISOString());

// === Pattern 3: Array append ===
// State variable: selectedIds (default: [])
const currentIds = selectedIds.value || [];
const newId = table1.selectedRow.data.id;
if (!currentIds.includes(newId)) {
  await selectedIds.setValue([...currentIds, newId]);
}

// === Pattern 4: Reset state on cancel ===
await formData.setValue({});
await selectedIds.setValue([]);
await isExpanded.setValue(false);

// === Pattern 5: Read after await (correct) ===
await counter.setValue(counter.value + 1);
console.log('New count:', counter.value); // Updated value
```

## Common mistakes

- **Reading state1.value immediately after state1.setValue() without await** — undefined Fix: Change state1.setValue(val) to await state1.setValue(val). Without the await keyword, you are reading the stale value because setValue() is a Promise.
- **Using state variables in transformers and wondering why updates do not fire** — undefined Fix: Transformers are read-only — they cannot call setValue() or trigger queries. Move your update logic to a JS Query instead.
- **Overwriting an entire object state variable when only one key needs changing** — undefined Fix: Use setIn(['keyName'], newValue) instead of setValue({ ...stateVar.value, keyName: newValue }). setIn() is safer and more concise.
- **Expecting state variables to persist after page navigation** — undefined Fix: 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.

## 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.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-use-state-variables-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-use-state-variables-in-retool
