---
title: Views
slug: /functions/views
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Views

Views are read-only functions that compute and return results from your tables. Unlike [reducers](/functions/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.

<Tabs groupId="server-language" queryString>
<TabItem value="rust" label="Rust">

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

```rust
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.

</TabItem>
<TabItem value="csharp" label="C#">

Use the `[SpacetimeDB.View]` attribute on a static method:

```csharp
using SpacetimeDB;

public static partial class Module
{
    [SpacetimeDB.Table]
    public partial struct Player
    {
        [SpacetimeDB.PrimaryKey]
        [SpacetimeDB.AutoInc]
        public ulong Id;

        [SpacetimeDB.Unique]
        public Identity Identity;

        public string Name;
    }

    [SpacetimeDB.Table]
    public partial struct PlayerLevel
    {
        [SpacetimeDB.Unique]
        public ulong PlayerId;

        [SpacetimeDB.Index.BTree]
        public ulong Level;
    }

    [SpacetimeDB.Type]
    public partial struct PlayerAndLevel
    {
        public ulong Id;
        public Identity Identity;
        public string Name;
        public ulong Level;
    }

    // At-most-one row: return T?
    [SpacetimeDB.View(Name = "MyPlayer", Public = true)]
    public static Player? MyPlayer(ViewContext ctx)
    {
        return ctx.Db.Player.Identity.Find(ctx.Sender) as Player;
    }

    // Multiple rows: return a list
    [SpacetimeDB.View(Name = "PlayersForLevel", Public = true)]
    public static List<PlayerAndLevel> PlayersForLevel(AnonymousViewContext ctx)
    {
        var rows = new List<PlayerAndLevel>();
        foreach (var player in ctx.Db.PlayerLevel.Level.Filter(1))
        {
            if (ctx.Db.Player.Id.Find(player.PlayerId) is Player p)
            {
                var row = new PlayerAndLevel
                {
                    Id = p.Id,
                    Identity = p.Identity,
                    Name = p.Name,
                    Level = player.Level
                };
                rows.Add(row);
            }
        }
        return rows;
    }
}
```

Views must be static methods and can return either a single row (`T?`) or a list of rows (`List<T>` or `T[]`) where `T` can be a table type or any product type.

</TabItem>
<TabItem value="typescript" label="TypeScript">

Use the `spacetimedb.view` or `spacetimedb.anonymousView` function:

```typescript
import { schema, table, t } from 'spacetimedb/server';

const players = table(
  { name: 'players', public: true },
  {
    id: t.u64().primaryKey().autoInc(),
    identity: t.identity().unique(),
    name: t.string(),
  }
);

const playerLevels = table(
  { name: 'player_levels', public: true },
  {
    player_id: t.u64().unique(),
    level: t.u64().index('btree'),
  }
);

const spacetimedb = schema(players, playerLevels);

// At-most-one row: return Option<row> via t.option(...)
// Your function may return the row or null
spacetimedb.view(
    { name: 'my_player', public: true },
    t.option(players.rowType),
    (ctx) => {
        const row = ctx.db.players.identity.find(ctx.sender);
        return row ?? undefined;
    }
);

// Define a custom row type for the joined result
const playerAndLevelRow = t.row('PlayerAndLevel', {
    id: t.u64(),
    name: t.string(),
    level: t.u64(),
});

// Multiple rows: return an array of rows via t.array(...)
spacetimedb.anonymousView(
    { name: 'players_for_level', public: true },
    t.array(playerAndLevelRow),
    (ctx) => {
        const out: Array<{ id: bigint; name: string; level: bigint }> = [];
        for (const playerLevel of ctx.db.playerLevels.level.filter(2n)) {
            const p = ctx.db.players.id.find(playerLevel.player_id);
            if (p) out.push({ id: p.id, name: p.name, level: playerLevel.level });
        }
        return out;
    }
);
```

The handler signature is `(ctx) => rows`, where `rows` must be either an array or option of product values.

</TabItem>
</Tabs>

## 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`.

<Tabs groupId="server-language" queryString>
<TabItem value="rust" label="Rust">

```rust
// 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()
}
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// View that depends on caller identity
[SpacetimeDB.View(Name = "MyPlayer", Public = true)]
public static Player? MyPlayer(ViewContext ctx)
{
    return ctx.Db.Player.Identity.Find(ctx.Sender) as Player;
}

// View that is the same for all callers
[SpacetimeDB.View(Name = "PlayersForLevel", Public = true)]
public static List<PlayerAndLevel> PlayersForLevel(AnonymousViewContext ctx)
{
    var rows = new List<PlayerAndLevel>();
    foreach (var player in ctx.Db.PlayerLevel.Level.Filter(1))
    {
        if (ctx.Db.Player.Id.Find(player.PlayerId) is Player p)
        {
            var row = new PlayerAndLevel
            {
                Id = p.Id,
                Identity = p.Identity,
                Name = p.Name,
                Level = player.Level
            };
            rows.Add(row);
        }
    }
    return rows;
}
```

</TabItem>
<TabItem value="typescript" label="TypeScript">

```typescript
// View that depends on caller identity
spacetimedb.view(
  { name: 'my_player', public: true },
  t.option(players.rowType),
  (ctx) => {
    const row = ctx.db.players.identity.find(ctx.sender);
    return row ?? undefined;
  }
);

// View that is the same for all callers
spacetimedb.anonymousView(
  { name: 'players_for_level', public: true },
  t.array(playerAndLevelRow),
  (ctx) => {
    const out: Array<{ id: bigint; name: string; level: bigint }> = [];
    for (const playerLevel of ctx.db.playerLevels.level.filter(2n)) {
      const p = ctx.db.players.id.find(playerLevel.player_id);
      if (p) out.push({ id: p.id, name: p.name, level: playerLevel.level });
    }
    return out;
  }
);
```

</TabItem>
</Tabs>

## Querying Views

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

```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

- Review [Subscriptions](/subscriptions) for real-time client data access
