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