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.
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:
Automatic Cell creation - The runtime creates the Cell for you
Default values - Specifies what value to use if none is provided
Schema generation - Generates proper JSON schema for your pattern
Works - also at the time of this writing,
cell()has some bugs, this is a huge reason to avoid it. When you useDefault<>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 32import { 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:
Create a Cell that holds a number
Initialize it to 100
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
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 12import { 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"]>:
First parameter: The type is an array of strings
Second parameter: The default value is an array with three todo items already in it
Now let’s add a handler to add new items to the list:
1 2 3 4 5 6 7 8 9 10 11 12const 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 46import { 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
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 16interface 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:
First parameter: The
PlayertypeSecond parameter: An object with default values for each property
Let’s create handlers to update the stats:
1 2 3 4 5 6 7 8 9 10 11 12 13const 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:
Use
stats.key("propertyName")to access a specific property as a CellCall
.get()on that property Cell to read the current valueCall
.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 59import { 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
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.