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