Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Tutorial

Using Defaults for Cells

Abstract

In this section, we learn how to use Default<> to automatically create and initialize Cells for pattern inputs. This is the recommended pattern for managing pattern state.

Keywords:commonfabricstateCellDefaultpattern inputs

Introduction

In previous chapters, we used cell() to create and manage state. While this works fine for learning, there’s a better way to handle state in patterns: using Default<> in your pattern’s input interface.

We introduced cell() first to help you understand how Cells work without learning too many concepts at once. Now that you’re comfortable with Cells, let’s learn the recommended pattern.

Important: For pattern inputs, you should almost always use Default<> instead of cell().

Why Use Default<>?

Default<Type, InitialValue> provides three key benefits:

  1. Automatic Cell creation - The runtime creates the Cell for you

  2. Default values - Specifies what value to use if none is provided

  3. Schema generation - Generates proper JSON schema for your pattern

  4. Works - also at the time of this writing, cell() has some bugs, this is a huge reason to avoid it. When you use Default<> in a pattern’s input interface, the Common Fabric runtime automatically creates a Cell and initializes it with your default value.

A Simple Counter with Default

Let’s create a counter using Default<>. Compare this to creating a Cell manually:

Using cell() (what we did before):

export default pattern(() => {
  const count = cell<number>(0); // Manual Cell creation

  return {
    [UI]: <div>{count}</div>,
  };
});

Using Default<> (recommended):

interface CounterState {
  count: Default<number, 100>;
}

export default pattern<CounterState>((state) => {
  // state.count is already a Cell<number> initialized to 100

  return {
    [UI]: <div>{state.count}</div>,
    count: state.count, // Export the cell
  };
});

Let’s build a complete counter with an increment button:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import {
  Default,
  h,
  handler,
  pattern,
  UI,
  type Cell,
} from "commonfabric";

interface CounterState {
  count: Default<number, 100>;
}

const increment = handler<unknown, { count: Cell<number> }>(
  (_, { count }) => {
    count.set(count.get() + 1);
  },
);

export default pattern<CounterState>((state) => {
  return {
    [UI]: (
      <div>
        <h2>Count: {state.count}</h2>
        <button type="button" onclick={increment({ count: state.count })}>
          Increment
        </button>
      </div>
    ),
    count: state.count,
  };
});

Lines 10-12 define the input interface with Default<number, 100>. This tells the runtime:

Line 20 receives state which has a count property that’s already a Cell<number>.

Line 25 passes the Cell to the handler, just like before.

Line 30 exports the Cell so other patterns can use it.

View complete code :animate: fade-in
using_defaults_counter.tsx
import { type Cell, Default, handler, pattern, UI } from "commonfabric";

interface CounterState {
  count: Default<number, 100>;
}

const increment = handler<unknown, { count: Cell<number> }>(
  (_, { count }) => {
    count.set(count.get() + 1);
  },
);

export default pattern<CounterState>((state) => {
  return {
    [UI]: (
      <div>
        <h2>Count: {state.count}</h2>
        <button type="button" onclick={increment({ count: state.count })}>
          Increment
        </button>
      </div>
    ),
    count: state.count,
  };
});

When you deploy this pattern, the counter starts at 100. If someone creates an instance of this pattern and provides a different value, it will start there instead.

Using Default with Arrays

Arrays are common in patterns. Let’s create a todo list that starts with a few items already in it:

1
2
3
4
5
6
7
8
9
10
11
12
import {
  Default,
  h,
  handler,
  pattern,
  UI,
  type Cell,
} from "commonfabric";

interface TodoListState {
  items: Default<string[], ["Pay bill", "Write code", "Dinner with friends"]>;
}

Line 11 specifies Default<string[], ["Pay bill", "Write code", "Dinner with friends"]>:

Now let’s add a handler to add new items to the list:

1
2
3
4
5
6
7
8
9
10
11
12
const addItem = handler<
  { detail: { message: string } },
  { items: Cell<string[]> }
>(
  (event, { items }) => {
    const value = event.detail.message?.trim();
    if (value) {
      const currentItems = items.get();
      items.set([...currentItems, value]);
    }
  },
);

This handler receives an event with {detail: {message: string}} - this is the shape we’ll need for the <cf-message-input> component we’ll use later. The handler gets the message text, trims whitespace, and adds it to the array if it’s not empty.

Here’s the complete todo list pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import {
  Default,
  h,
  handler,
  pattern,
  UI,
  type Cell,
} from "commonfabric";

interface TodoListState {
  items: Default<string[], ["Pay bill", "Write code", "Dinner with friends"]>;
}

const addItem = handler<
  { detail: { message: string } },
  { items: Cell<string[]> }
>(
  (event, { items }) => {
    const value = event.detail.message?.trim();
    if (value) {
      const currentItems = items.get();
      items.set([...currentItems, value]);
    }
  },
);

export default pattern<TodoListState>((state) => {
  return {
    [UI]: (
      <div>
        <h2>My Todos</h2>
        <cf-message-input
          name="Add"
          placeholder="Add a todo..."
          oncf-send={addItem({ items: state.items })}
        />
        <ul>
          {state.items.map((item) => (
            <li>{item}</li>
          ))}
        </ul>
      </div>
    ),
    items: state.items,
  };
});

Lines 10-12 define the state with three todo items as the default.

Line 27 receives state which has an items property that’s already a Cell<string[]>.

Lines 32-35 use the <cf-message-input> component for input, which provides a text field with a submit button. The oncf-send event fires when the user submits.

Lines 38-40 use .map() to render each item in the list.

View complete code :animate: fade-in
using_defaults_array.tsx
import { type Cell, Default, handler, pattern, UI } from "commonfabric";

interface TodoListState {
  items: Default<string[], ["Pay bill", "Write code", "Dinner with friends"]>;
}

const addItem = handler<
  { detail: { message: string } },
  { items: Cell<string[]> }
>(
  (event, { items }) => {
    const value = event.detail.message?.trim();
    if (value) {
      const currentItems = items.get();
      items.set([...currentItems, value]);
    }
  },
);

export default pattern<TodoListState>((state) => {
  return {
    [UI]: (
      <div>
        <h2>My Todos</h2>
        <cf-message-input
          name="Add"
          placeholder="Add a todo..."
          oncf-send={addItem({ items: state.items })}
        />
        <ul>
          {/* Note: key is not needed for Common Fabric but linters require it */}
          {state.items.map((item, index) => <li key={index}>{item}</li>)}
        </ul>
      </div>
    ),
    items: state.items,
  };
});

Using Default with Complex Objects

Default<> works with any type, including complex objects. Let’s create a game stats tracker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Player {
  playerName: string;
  score: number;
  level: number;
}

interface GameState {
  stats: Default<
    Player,
    {
      playerName: "Player 1";
      score: 500;
      level: 10;
    }
  >;
}

Lines 1-5 define the TypeScript interface for our game stats.

Lines 7-17 use Default<> with:

Let’s create handlers to update the stats:

1
2
3
4
5
6
7
8
9
10
11
12
13
const incrementScore = handler<unknown, { stats: Cell<Player> }>(
  (_, { stats }) => {
    const currentScore = stats.key("score").get();
    stats.key("score").set(currentScore + 10);
  },
);

const levelUp = handler<unknown, { stats: Cell<Player> }>(
  (_, { stats }) => {
    const currentLevel = stats.key("level").get();
    stats.key("level").set(currentLevel + 1);
  },
);

Both handlers use the .key() pattern to update individual properties:

  1. Use stats.key("propertyName") to access a specific property as a Cell

  2. Call .get() on that property Cell to read the current value

  3. Call .set() on that property Cell to update just that property

This approach is cleaner than reconstructing the entire object and avoids issues with the spread operator. Notice that levelUp only updates the level - it doesn’t touch the score, which continues to accumulate.

Here’s the complete game stats pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import {
  Default,
  h,
  handler,
  pattern,
  UI,
  type Cell,
} from "commonfabric";

interface Player {
  playerName: string;
  score: number;
  level: number;
}

interface GameState {
  stats: Default<
    Player,
    {
      playerName: "Player 1";
      score: 500;
      level: 10;
    }
  >;
}

const incrementScore = handler<unknown, { stats: Cell<GameStats> }>(
  (_, { stats }) => {
    const currentScore = stats.key("score").get();
    stats.key("score").set(currentScore + 10);
  },
);

const levelUp = handler<unknown, { stats: Cell<Player> }>(
  (_, { stats }) => {
    const currentLevel = stats.key("level").get();
    stats.key("level").set(currentLevel + 1);
  },
);

export default pattern<GameState>((state) => {
  return {
    [UI]: (
      <div>
        <h2>Game Stats</h2>
        <p>Player: {state.stats.playerName}</p>
        <p>Level: {state.stats.level}</p>
        <p>Score: {state.stats.score}</p>
        <button type="button" onclick={incrementScore({ stats: state.stats })}>
          Add 10 Points
        </button>
        <button type="button" onclick={levelUp({ stats: state.stats })}>
          Level Up
        </button>
      </div>
    ),
    stats: state.stats,
  };
});

Lines 11-24 define both the stats type and the default state.

Lines 50-52 access properties of the stats object in the UI. Notice we can access nested properties like state.stats.playerName directly in JSX.

Lines 53-58 pass the entire stats Cell to handlers that update different properties.

View complete code :animate: fade-in
using_defaults_object.tsx
import { type Cell, Default, handler, pattern, UI } from "commonfabric";

interface Player {
  playerName: string;
  score: number;
  level: number;
}

interface GameState {
  stats: Default<
    Player,
    {
      playerName: "Player 1";
      score: 500;
      level: 10;
    }
  >;
}

const incrementScore = handler<unknown, { stats: Cell<Player> }>(
  (_, { stats }) => {
    const currentScore = stats.key("score").get();
    stats.key("score").set(currentScore + 10);
  },
);

const levelUp = handler<unknown, { stats: Cell<Player> }>(
  (_, { stats }) => {
    const currentLevel = stats.key("level").get();
    stats.key("level").set(currentLevel + 1);
  },
);

export default pattern<GameState>((state) => {
  return {
    [UI]: (
      <div>
        <h2>Game Stats</h2>
        <p>Player: {state.stats.playerName}</p>
        <p>Level: {state.stats.level}</p>
        <p>Score: {state.stats.score}</p>
        <button type="button" onclick={incrementScore({ stats: state.stats })}>
          Add 10 Points
        </button>
        <button type="button" onclick={levelUp({ stats: state.stats })}>
          Level Up
        </button>
      </div>
    ),
    stats: state.stats,
  };
});

When you deploy this pattern, it starts with the default player name, level 10, and score 500.

Key Takeaways

We’ve learned how to use Default<> to create Cells automatically:

For simple values:

interface State {
  count: Default<number, 100>;
}

For arrays:

interface State {
  items: Default<string[], ["Pay bill", "Write code", "Dinner with friends"]>;
}

For objects:

interface State {
  stats: Default<
    Player,
    { playerName: "Player 1"; score: 500; level: 10 }
  >;
}

In the next chapter, we’ll explore working with lists in more detail, including adding, removing, and editing items.