Debounce with ChangeListener
Use ChangeListener with throttleWaitInMs to delay reactions to value changes, reducing unnecessary operations. The following example implements search within a product catalog (sample products are Laptop, Mouse, Keyboard, etc.) using the ChangeListener component to throttle API calls.
<Component
name="DebouncedSearch"
var.searchTerm=""
var.results="{[]}"
var.inProgress="{false}">
<VStack>
<TextBox
id="searchInput"
label="Search products:"
placeholder="Type to search..."
value="{searchTerm}"
onDidChange="e => searchTerm = e"
/>
<ChangeListener
listenTo="{searchTerm}"
throttleWaitInMs="500"
onDidChange="arg => {
if (!arg.newValue) {
results = [];
inProgress = false;
return;
}
inProgress = true;
const response = Actions.callApi({
url: '/api/search',
method: 'POST',
body: { query: arg.newValue }
});
results = response.status === 'ok' ? response.results : [];
inProgress = false;
}"
/>
<Card when="{searchTerm.length > 0}">
<VStack>
<Text when="{inProgress}" variant="em">
Searching for: {searchTerm}
</Text>
<Fragment when="{!inProgress}">
<Fragment when="{results.length > 0}">
<H4>Found {pluralize(results.length, 'result', 'results')}</H4>
<List data="{results}">
{$item.name} ({$item.category}) - ${$item.price}
</List>
</Fragment>
<Text when="{results.length === 0}">
No results found
</Text>
</Fragment>
</VStack>
</Card>
</VStack>
</Component><App>
<DebouncedSearch />
</App>Search with ChangeListener throttling
<Component
name="DebouncedSearch"
var.searchTerm=""
var.results="{[]}"
var.inProgress="{false}">
<VStack>
<TextBox
id="searchInput"
label="Search products:"
placeholder="Type to search..."
value="{searchTerm}"
onDidChange="e => searchTerm = e"
/>
<ChangeListener
listenTo="{searchTerm}"
throttleWaitInMs="500"
onDidChange="arg => {
if (!arg.newValue) {
results = [];
inProgress = false;
return;
}
inProgress = true;
const response = Actions.callApi({
url: '/api/search',
method: 'POST',
body: { query: arg.newValue }
});
results = response.status === 'ok' ? response.results : [];
inProgress = false;
}"
/>
<Card when="{searchTerm.length > 0}">
<VStack>
<Text when="{inProgress}" variant="em">
Searching for: {searchTerm}
</Text>
<Fragment when="{!inProgress}">
<Fragment when="{results.length > 0}">
<H4>Found {pluralize(results.length, 'result', 'results')}</H4>
<List data="{results}">
{$item.name} ({$item.category}) - ${$item.price}
</List>
</Fragment>
<Text when="{results.length === 0}">
No results found
</Text>
</Fragment>
</VStack>
</Card>
</VStack>
</Component><App>
<DebouncedSearch />
</App>Key Points
Listen to the right value: Use listenTo to watch the specific variable or component property that drives your logic:
<!-- ✅ Correct - listens to the searchTerm variable -->
<ChangeListener
listenTo="{searchTerm}"
throttleWaitInMs="500"
onDidChange="arg => handleSearch(arg.newValue)"
/>
<!-- ✅ Also correct - listens to component's value property -->
<ChangeListener
listenTo="{searchInput.value}"
throttleWaitInMs="500"
onDidChange="arg => handleSearch(arg.newValue)"
/>Access previous and new values: The event argument provides both prevValue and newValue for comparison:
<ChangeListener
listenTo="{userInput}"
throttleWaitInMs="300"
onDidChange="arg => {
if (!arg.prevValue && !arg.newValue) return; // Skip initial empty state
console.log(`Changed from '${arg.prevValue}' to '${arg.newValue}'`);
}"
/>Handle empty values gracefully: Consider what should happen when the watched value becomes empty:
<ChangeListener
listenTo="{searchTerm}"
throttleWaitInMs="500"
onDidChange="arg => {
if (!arg.newValue) {
results = []; // Clear results when search is empty
return;
}
// Perform search with arg.newValue
}"
/>Common use cases:
- Search inputs — throttle API calls as user types
- Form validation — delay validation until user pauses
- Auto-save — save changes after editing stops
- Filters and sorting — reduce computation on rapid changes
- State synchronization — coordinate between components