Update UI optimistically
Use local reactive variables to reflect state changes instantly while the API call runs in the background.
Users expect immediate feedback when they tap a like button or toggle a setting. Instead of waiting for the server round-trip, update a local variable right away and fire the API call in parallel. If the call fails, you can roll back — but in the common success case the UI feels instant.
<App>
<APICall
id="favoritePost"
method="post"
url="/api/posts/{$param}/favorite"
inProgressNotificationMessage="Favoriting post..."
completedNotificationMessage="Post favorited!" />
<APICall
id="unfavoritePost"
method="post"
url="/api/posts/{$param}/unfavorite"
inProgressNotificationMessage="Unfavoriting post..."
completedNotificationMessage="Post unfavorited!" />
<DataSource
id="timelineData"
url="/api/timeline"
method="GET" />
<VStack>
<Items data="{timelineData}">
<Card var.localFavorited="{null}" var.localFavoritesCount="{null}">
<VStack>
<Text>{$item.author}</Text>
<Text>{$item.content}</Text>
<HStack verticalAlignment="center">
<HStack verticalAlignment="center">
<SocialButton icon="reply" />
<Text variant="caption">{$item.replies_count}</Text>
</HStack>
<HStack verticalAlignment="center">
<SocialButton icon="trending-up" />
<Text variant="caption">{$item.reblogs_count}</Text>
</HStack>
<HStack verticalAlignment="center">
<SocialButton
icon="like"
themeColor="{(localFavorited !== null
? localFavorited : $item.favourited)
? 'attention' : 'secondary'}"
onClick="
// Get current state (local takes precedence)
const currentFavorited = localFavorited !== null
? localFavorited
: $item.favourited;
const currentCount = localFavoritesCount !== null
? localFavoritesCount
: ($item.favourites_count || 0);
// Update UI optimistically
localFavorited = !currentFavorited;
localFavoritesCount = currentFavorited ?
Math.max(0, currentCount - 1) :
currentCount + 1;
// Make API call
if (currentFavorited) {
unfavoritePost.execute($item.id);
timelineData.refetch();
} else {
favoritePost.execute($item.id);
timelineData.refetch();
}
">
</SocialButton>
<Text variant="caption">
{localFavoritesCount !== null
? localFavoritesCount : ($item.favourites_count || 0)}
</Text>
</HStack>
</HStack>
</VStack>
</Card>
</Items>
</VStack>
</App><Component name="SocialButton">
<Button
borderRadius="50%"
icon="{$props.icon}"
variant="outlined"
themeColor="{$props.themeColor || 'secondary'}"
size="xs"
onClick="{emitEvent('click')}" />
</Component><App>
<APICall
id="favoritePost"
method="post"
url="/api/posts/{$param}/favorite"
inProgressNotificationMessage="Favoriting post..."
completedNotificationMessage="Post favorited!" />
<APICall
id="unfavoritePost"
method="post"
url="/api/posts/{$param}/unfavorite"
inProgressNotificationMessage="Unfavoriting post..."
completedNotificationMessage="Post unfavorited!" />
<DataSource
id="timelineData"
url="/api/timeline"
method="GET" />
<VStack>
<Items data="{timelineData}">
<Card var.localFavorited="{null}" var.localFavoritesCount="{null}">
<VStack>
<Text>{$item.author}</Text>
<Text>{$item.content}</Text>
<HStack verticalAlignment="center">
<HStack verticalAlignment="center">
<SocialButton icon="reply" />
<Text variant="caption">{$item.replies_count}</Text>
</HStack>
<HStack verticalAlignment="center">
<SocialButton icon="trending-up" />
<Text variant="caption">{$item.reblogs_count}</Text>
</HStack>
<HStack verticalAlignment="center">
<SocialButton
icon="like"
themeColor="{(localFavorited !== null
? localFavorited : $item.favourited)
? 'attention' : 'secondary'}"
onClick="
// Get current state (local takes precedence)
const currentFavorited = localFavorited !== null
? localFavorited
: $item.favourited;
const currentCount = localFavoritesCount !== null
? localFavoritesCount
: ($item.favourites_count || 0);
// Update UI optimistically
localFavorited = !currentFavorited;
localFavoritesCount = currentFavorited ?
Math.max(0, currentCount - 1) :
currentCount + 1;
// Make API call
if (currentFavorited) {
unfavoritePost.execute($item.id);
timelineData.refetch();
} else {
favoritePost.execute($item.id);
timelineData.refetch();
}
">
</SocialButton>
<Text variant="caption">
{localFavoritesCount !== null
? localFavoritesCount : ($item.favourites_count || 0)}
</Text>
</HStack>
</HStack>
</VStack>
</Card>
</Items>
</VStack>
</App><Component name="SocialButton">
<Button
borderRadius="50%"
icon="{$props.icon}"
variant="outlined"
themeColor="{$props.themeColor || 'secondary'}"
size="xs"
onClick="{emitEvent('click')}" />
</Component>The relationship between onClick="{emitEvent('click')}" in the SocialButton component and the <event name="click"> handler in the main app demonstrates event propagation in XMLUI.
Key points
Local variables provide instant feedback: Declare var.localFavorited and var.localFavoritesCount on the Card. Update them synchronously before firing the API call. The UI re-renders immediately because these are reactive variables.
null signals "use the server value": Initialize both locals to null. The ternary localFavorited !== null ? localFavorited : $item.favourited falls back to the data-source value until the user interacts. After the timelineData.refetch() completes, the server data catches up and the local override is no longer needed.
The API call runs in the background: execute() returns a Promise. The UI has already updated before the server responds. Chain timelineData.refetch() to pull the authoritative state back from the server.
Component reuse through emitEvent: SocialButton doesn't know what a click should do. It calls emitEvent('click') and the parent handles the business logic in an onClick event handler. Different instances of SocialButton can handle clicks differently.
See also
- Chain a DataSource refetch — the non-optimistic version of the same pattern
- Invalidate related data after a write — declarative cache refresh
- Retry a failed API call — handling failures when the optimistic assumption was wrong