Toggle multiple items in a list with shared state
When users toggle checkboxes in a list and each toggle accumulates into a shared array (e.g., hiding sources, selecting categories), use a global variable for the shared state and update it optimistically on each click. Don't refetch the DataSource after each toggle — the refetch can overwrite your optimistic update before the API call completes.
<App
global.hiddenCategories="{[]}"
var.articles="{[
{ id: 1, title: 'Understanding React Hooks', category: 'Technology' },
{ id: 2, title: 'Best Hiking Trails in Colorado', category: 'Outdoors' },
{ id: 3, title: 'New Jazz Album Reviews', category: 'Music' },
{ id: 4, title: 'Building REST APIs with Node', category: 'Technology' },
{ id: 5, title: 'Weekend Camping Essentials', category: 'Outdoors' },
{ id: 6, title: 'Live Concert Guide: February', category: 'Music' },
{ id: 7, title: 'CSS Grid Layout Patterns', category: 'Technology' },
{ id: 8, title: 'Mountain Biking for Beginners', category: 'Outdoors' },
{ id: 9, title: 'Classical Piano Performances', category: 'Music' },
{ id: 10, title: 'Local Farmers Market Schedule', category: 'Food' },
{ id: 11, title: 'Best Bakeries Downtown', category: 'Food' },
{ id: 12, title: 'TypeScript Migration Guide', category: 'Technology' }
]}">
<CategoryFilter
categories="{['Technology', 'Outdoors', 'Music', 'Food'].map(
(name) => ({ name: name, count: articles.filter((a) => a.category === name).length })
)}" />
<H2>Articles</H2>
<Text variant="caption" color="$color-text-secondary">
{articles.filter((a) => hiddenCategories.indexOf(a.category) === -1).length} of {articles.length} shown
</Text>
<Items data="{articles.filter((a) => hiddenCategories.indexOf(a.category) === -1)}">
<Card padding="$space-2" marginBottom="$space-1">
<VStack gap="$space-0">
<Text fontWeight="bold">{$item.title}</Text>
<Text fontSize="$fontSize-xs" fontStyle="italic" color="$color-text-tertiary">{$item.category}</Text>
</VStack>
</Card>
</Items>
</App><Component name="CategoryFilter">
<VStack gap="$space-2" padding="$space-4">
<H2>Categories</H2>
<Text variant="caption" color="$color-text-secondary">
Uncheck a category to hide its articles
</Text>
<Items data="{$props.categories}">
<HStack gap="$space-2" verticalAlignment="center">
<Checkbox
initialValue="{hiddenCategories.indexOf($item.name) === -1}"
onClick="hiddenCategories = hiddenCategories.indexOf($item.name) >= 0
? hiddenCategories.filter((x) => x !== $item.name)
: [...hiddenCategories, $item.name]"
/>
<Text fontWeight="bold" width="30px" textAlign="right">{$item.count}</Text>
<Text>{$item.name}</Text>
</HStack>
</Items>
</VStack>
</Component><App
global.hiddenCategories="{[]}"
var.articles="{[
{ id: 1, title: 'Understanding React Hooks', category: 'Technology' },
{ id: 2, title: 'Best Hiking Trails in Colorado', category: 'Outdoors' },
{ id: 3, title: 'New Jazz Album Reviews', category: 'Music' },
{ id: 4, title: 'Building REST APIs with Node', category: 'Technology' },
{ id: 5, title: 'Weekend Camping Essentials', category: 'Outdoors' },
{ id: 6, title: 'Live Concert Guide: February', category: 'Music' },
{ id: 7, title: 'CSS Grid Layout Patterns', category: 'Technology' },
{ id: 8, title: 'Mountain Biking for Beginners', category: 'Outdoors' },
{ id: 9, title: 'Classical Piano Performances', category: 'Music' },
{ id: 10, title: 'Local Farmers Market Schedule', category: 'Food' },
{ id: 11, title: 'Best Bakeries Downtown', category: 'Food' },
{ id: 12, title: 'TypeScript Migration Guide', category: 'Technology' }
]}">
<CategoryFilter
categories="{['Technology', 'Outdoors', 'Music', 'Food'].map(
(name) => ({ name: name, count: articles.filter((a) => a.category === name).length })
)}" />
<H2>Articles</H2>
<Text variant="caption" color="$color-text-secondary">
{articles.filter((a) => hiddenCategories.indexOf(a.category) === -1).length} of {articles.length} shown
</Text>
<Items data="{articles.filter((a) => hiddenCategories.indexOf(a.category) === -1)}">
<Card padding="$space-2" marginBottom="$space-1">
<VStack gap="$space-0">
<Text fontWeight="bold">{$item.title}</Text>
<Text fontSize="$fontSize-xs" fontStyle="italic" color="$color-text-tertiary">{$item.category}</Text>
</VStack>
</Card>
</Items>
</App><Component name="CategoryFilter">
<VStack gap="$space-2" padding="$space-4">
<H2>Categories</H2>
<Text variant="caption" color="$color-text-secondary">
Uncheck a category to hide its articles
</Text>
<Items data="{$props.categories}">
<HStack gap="$space-2" verticalAlignment="center">
<Checkbox
initialValue="{hiddenCategories.indexOf($item.name) === -1}"
onClick="hiddenCategories = hiddenCategories.indexOf($item.name) >= 0
? hiddenCategories.filter((x) => x !== $item.name)
: [...hiddenCategories, $item.name]"
/>
<Text fontWeight="bold" width="30px" textAlign="right">{$item.count}</Text>
<Text>{$item.name}</Text>
</HStack>
</Items>
</VStack>
</Component>The key points:
-
hiddenCategoriesis a global variable declared withglobal.hiddenCategorieson the App. TheCategoryFiltercomponent reads and writes it directly — no prop drilling needed. This differs from the optimistic UI pattern where each item has independent local state. -
No DataSource refetch after toggle. Assigning to
hiddenCategoriesis enough to trigger XMLUI's reactivity — the filtered articles list and checkbox states both update immediately. Refetching after an optimistic update risks overwriting it with stale server data (the API call may not have completed yet). -
Checkbox without
readOnly. AreadOnlycheckbox only changes visually when itsinitialValueexpression is re-evaluated during a re-render. Without a DataSource refetch or other trigger, that re-render may not happen. OmittingreadOnlylets the checkbox toggle visually on click, independent of re-render timing. -
initialValuedrives the initial state. On first render, each checkbox reads from thehiddenCategoriesarray. On subsequent clicks, the checkbox toggles visually on its own, and theonClickhandler keepshiddenCategoriesin sync for the filtered list.
Persisting to a server
In a real application, you'd also save the hidden categories to an API. Fire the API call without waiting for it — the optimistic update has already updated the UI:
<Checkbox
initialValue="{hiddenCategories.indexOf($item.name) === -1}"
onClick="
hiddenCategories = hiddenCategories.indexOf($item.name) >= 0
? hiddenCategories.filter((x) => x !== $item.name)
: [...hiddenCategories, $item.name];
savePreferences.execute({ hidden: hiddenCategories });
"
/>
Use invalidates="{[]}" on the APICall to prevent it from refetching DataSources that would overwrite your optimistic state.