Skip to main content

Views

Views are read-only functions that compute and return results from your tables. Unlike reducers, views do not modify database state - they only query and return data. Views are useful for computing derived data, aggregations, or joining multiple tables before sending results to clients.

Why Use Views?

Views provide several benefits:

  • Performance: Views compute results server-side, reducing the amount of data sent to clients
  • Encapsulation: Views can hide complex queries behind simple interfaces
  • Consistency: Views ensure clients receive consistently formatted data
  • Real-time updates: Like tables, views can be subscribed to and automatically update when underlying data changes

Defining Views

Views must be declared as public with an explicit name, and they accept only a context parameter - no user-defined arguments beyond the context type.

Use the #[spacetimedb::view] macro on a function:

use spacetimedb::{view, ViewContext, AnonymousViewContext, table, SpacetimeType}; use spacetimedb_lib::Identity;  #[spacetimedb::table(name = player)] pub struct Player {  #[primary_key]  #[auto_inc]  id: u64,  #[unique]  identity: Identity,  name: String, }  #[spacetimedb::table(name = player_level)] pub struct PlayerLevel {  #[unique]  player_id: u64,  #[index(btree)]  level: u64, }  #[derive(SpacetimeType)] pub struct PlayerAndLevel {  id: u64,  identity: Identity,  name: String,  level: u64, }  // At-most-one row: return Option<T> #[view(name = my_player, public)] fn my_player(ctx: &ViewContext) -> Option<Player> {  ctx.db.player().identity().find(ctx.sender) }  // Multiple rows: return Vec<T> #[view(name = players_for_level, public)] fn players_for_level(ctx: &AnonymousViewContext) -> Vec<PlayerAndLevel> {  ctx.db  .player_level()  .level()  .filter(2u64)  .flat_map(|player| {  ctx.db  .player()  .id()  .find(player.player_id)  .map(|p| PlayerAndLevel {  id: p.id,  identity: p.identity,  name: p.name,  level: player.level,  })  })  .collect() }

Views can return either Option<T> for at-most-one row or Vec<T> for multiple rows, where T can be a table type or any product type.

ViewContext and AnonymousViewContext

Views use one of two context types:

  • ViewContext: Provides access to the caller's Identity through ctx.sender. Use this when the view depends on who is querying it.
  • AnonymousViewContext: Does not provide caller information. Use this when the view produces the same results regardless of who queries it.

Both contexts provide read-only access to tables and indexes through ctx.db.

// View that depends on caller identity #[view(name = my_player, public)] fn my_player(ctx: &ViewContext) -> Option<Player> {  ctx.db.player().identity().find(ctx.sender) }  // View that is the same for all callers #[view(name = players_for_level, public)] fn players_for_level(ctx: &AnonymousViewContext) -> Vec<PlayerAndLevel> {  ctx.db  .player_level()  .level()  .filter(2u64)  .flat_map(|player| {  ctx.db  .player()  .id()  .find(player.player_id)  .map(|p| PlayerAndLevel {  id: p.id,  identity: p.identity,  name: p.name,  level: player.level,  })  })  .collect() }

Querying Views

Views can be queried and subscribed to just like normal tables using SQL:

SELECT * FROM my_player; SELECT * FROM players_for_level;

When subscribed to, views automatically update when their underlying tables change, providing real-time updates to clients.

Performance Considerations

Views compute results on the server side, which can improve performance by:

  • Reducing network traffic by filtering/aggregating before sending data
  • Avoiding redundant computation on multiple clients
  • Leveraging server-side indexes and query optimization

However, keep in mind that:

  • Complex views with multiple joins or aggregations can be expensive to compute
  • Views are recomputed whenever their underlying tables change
  • Subscriptions to views will receive updates even if the final result doesn't change

Next Steps