Skip to article frontmatterSkip to article content
Tutorial

Modifying State

Abstract

In this section, we will add on to the simple state we created in the last section. We will use user input to modify the existing state.

Keywords:commontoolsstateCelldatabase

Introduction

In the last section, we learned how to create state via Cells and also how to create derived states. We used building a fantasy game character sheet as an example. We’ll continue with that to learn how to modify state.

Handling User Input

Let’s start with changing our character’s name. We’ll need to add a text input field in the [UI] section. We can’t just use regular HTML components. The Common Tools runtime has its own JSX components to make sure data is protected and not accessed by other scripts.

    <common-send-message
      name="Update"
      placeholder="Update Name"
      // we need to fill out the event listener attribute below
      // onmessagesend= 
    />

If you deploy this update, you’ll see an input field, but nothing happens when you enter data. As the comments indicate, we need to fill out code for the onmessagesend JSX event listener.

This is when we learn about handler. A handler is a Common Tools runtime component that, like its name suggests, handles events. The JSX event listener (such as onmessagesend in our code) will call our handler to handle the event emitted by the JSX component.

Understanding Handlers

Handlers in Common Tools have a specific signature:

handler<EventType, ArgsType>(handlerFunction)

The handler function takes:

Detailed explanation

The handler function returns a factory that you call with your actual arguments to create the event handler. This factory pattern allows the handler to bind specific values from your recipe while still receiving events from the UI components.

We’ll start by writing our handler which takes the event emitted by the <common-send-message> component. This component emits a CustomEvent with the structure {detail: {message: string}}, where message contains the text the user entered. The handler will also take in the characterName cell. It will simply set the cell with the new name from the event.

Creating the Handler

const updateName = handler<
  { detail: { message: string } },
  { characterName: Cell<string> }
>(
  (event, { characterName }) => {
    console.log("Updating character name to:", event.detail.message);
    characterName.set(event.detail.message);
  }
);

Note that characterName was passed in as a Cell. We created it via the cell() function, which returns us a Cell. It’s important to mark reactive components as Cell so that we can call methods such as set() on them.

Now we can attach this handler to our input component:

<common-send-message
  name="Update"
  placeholder="Update Name"
  onmessagesend={updateName({ characterName })}
/>

If you deploy this code, you should see something like: Figure: Updating your character’s name

View complete code
state_02.tsx
/// <cts-enable />
import {
  cell,
  h,
  recipe,
  UI,
  lift,
  derive,
  handler,
  type Cell,
} from "commontools";

const calcAC = (dex: number) : number =>
  20 + Math.floor((dex - 10) / 2);

const updateName = handler<
  { detail: { message: string } },
  { characterName: Cell<string> }
>(
  (event, { characterName }) => {
    console.log("Updating character name to:", event.detail.message);
    characterName.set(event.detail.message);
  }
);

export default recipe("state test", () => {
  const characterName = cell<string>("");
  characterName.set("Lady Ellyxir");
  const dex = cell<number>(16);
  const ac = lift(calcAC)(dex);

  return {
    [UI]: (
      <div>
        <h2>Character name: {characterName}</h2>
        <common-send-message
          name="Update"
          placeholder="Update Name"
          onmessagesend={updateName({ characterName })}
        />
        <li>DEX: {dex}</li>
        <li>DEX Modifier: {Math.floor((dex - 10) / 2)}</li>
        <li>AC: {ac}</li>
      </div>
    ),
  };
});

Et voilà ! We’ve fully implemented modifying state through user input.

Detailed explanation

Each cell is created with a cause that uniquely identifies it. We carefully construct the cause so that it remains the same each time a recipe is run, but also unique from other cells created. This leads to automatic persistence when using the Common Tools runtime.

Adding Buttons

We’ll create a button to roll “dice” for the character’s Dexterity value. This will update the existing value.

First let’s create the handler for the click event. We don’t need details on the event itself, so we mark it as unknown.

const rollD6 = () => Math.floor(Math.random() * 6) + 1;

const rollDex = handler<
  unknown,
  Cell<number>
>(
  (_, dex) => {
    // Roll 3d6 for new DEX value
    const roll = rollD6() + rollD6() + rollD6();
    dex.set(roll);
  }
);

This handler simulates rolling three six-sided dice (3d6) and sets the DEX value to the result.

Next, we’ll add a button beside DEX in the UI and attach our handler:

<li>
  DEX: {dex}
  {" "}
  <ct-button onClick={rollDex(dex)}>
    Roll
  </ct-button>
</li>

Note the {" "} between the DEX value and button - this adds just a little padding before the button.

When we click on the button, the elements that depend on the value of that cell are also updated. This means the DEX, DEX Modifier, and AC values are all updated.

You should see something like the following once you click on the Roll button:

View complete code
state_03.tsx
/// <cts-enable />
import {
  cell,
  h,
  recipe,
  UI,
  lift,
  derive,
  handler,
  type Cell,
} from "commontools";

const calcAC = (dex: number) : number =>
  20 + Math.floor((dex - 10) / 2);

const updateName = handler<
  { detail: { message: string } },
  { characterName: Cell<string> }
>(
  (event, { characterName }) => {
    characterName.set(event.detail.message);
  }
);

const rollD6 = () => Math.floor(Math.random() * 6) + 1;

const rollDex = handler<
  unknown,
  Cell<number>
>(
  (_, dex) => {
    // Roll 3d6 for new DEX value
    const roll = rollD6() + rollD6() + rollD6();
    dex.set(roll);
  }
);

export default recipe("state test", () => {
  const characterName = cell<string>("");
  characterName.set("Lady Ellyxir");
  const dex = cell<number>(16);
  const ac = lift(calcAC)(dex);

  return {
    [UI]: (
      <div>
        <h2>Character name: {characterName}</h2>
        <common-send-message
          name="Update"
          placeholder="Update Name"
          onmessagesend={updateName({ characterName })}
        />
        <li>
          DEX: {dex}
          {" "}
          <ct-button onClick={rollDex(dex)}>
            Roll
          </ct-button>
        </li>
        <li>DEX Modifier: {Math.floor((dex - 10) / 2)}</li>
        <li>AC: {ac}</li>
      </div>
    ),
  };
});

So far, we’ve been using Cell to store primitive data types. In the next section, we’ll move on to objects and arrays.