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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 10-15 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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 }}.
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 }}.
1// In the Text component's value field:2{{ `Current filter: ${filterState.value}` }}34// In a Button's Disabled property:5{{ isLoading.value }}67// 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.
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.
1// JS Query: toggleModal2// WRONG — do not read immediately without await3isModalOpen.setValue(true);4console.log(isModalOpen.value); // Still false!56// CORRECT — await the Promise7await isModalOpen.setValue(true);8console.log(isModalOpen.value); // true910// Toggle pattern11await isModalOpen.setValue(!isModalOpen.value);Expected result: The state variable updates correctly and components that reference {{ isModalOpen.value }} re-render.
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.
1// State variable 'formData' with default value: {}2// { name: '', email: '', address: { city: '', zip: '' } }34// Update top-level key5await formData.setIn(['name'], textInput1.value);67// Update nested key8await formData.setIn(['address', 'city'], cityInput.value);910// Read the full updated object11console.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.
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.
1// JS Query: toggleAdvanced (triggered by Button click)2await showAdvanced.setValue(!showAdvanced.value);34// Button label (in Inspector → General → Label):5{{ showAdvanced.value ? 'Hide advanced options' : 'Show advanced options' }}67// 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.
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.
1// Step 1 Next button — JS Query: saveStep12await formData.setIn(['firstName'], firstNameInput.value);3await formData.setIn(['lastName'], lastNameInput.value);4await formData.setIn(['email'], emailInput.value);5// Advance to next tab6await tabbedContainer1.setCurrentTab(1);78// Step 2 Next button — JS Query: saveStep29await formData.setIn(['company'], companyInput.value);10await formData.setIn(['role'], roleSelect.value);11await tabbedContainer1.setCurrentTab(2);1213// Final Submit button — JS Query: submitForm14const payload = formData.value;15await createUserRecord.trigger({16 additionalScope: { userData: payload }17});18await formData.setValue({}); // Reset after submitExpected result: Form data accumulates across steps in the state variable and is submitted as a complete object on the final step.
Complete working example
1// === Pattern 1: Simple toggle ===2// State variable: isExpanded (default: false)3await isExpanded.setValue(!isExpanded.value);45// === 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());1011// === 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}1819// === Pattern 4: Reset state on cancel ===20await formData.setValue({});21await selectedIds.setValue([]);22await isExpanded.setValue(false);2324// === Pattern 5: Read after await (correct) ===25await counter.setValue(counter.value + 1);26console.log('New count:', counter.value); // Updated valueCommon 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation