Skip to main content

Page Action | Update

Easily create reactive apps by updating the UI with page.update(). Compose will diff the previous UI against the new one and intelligently update any components that have changed.

import { Compose, Page, UI } from "@composehq/sdk"

function handler({ page, ui }: { page: Page, ui: UI }) {
let count = 0;

page.add(() => ui.stack([
ui.header(`Count: ${count}`),
ui.button("increment-btn", {
label: "Increment",
onClick: () => {
count += 1
page.update()
}
}),
]))
}

const app = new Compose.App({ route: "counter", handler })

Note: page.update() was introduced in version 0.21.0 of the SDK.

How it works

You may have noticed that page.add() and page.modal() both accept callback functions that return UI components, instead of accepting the UI components directly.

When you call page.update(), Compose will execute the callback functions again and diff the result against the previously cached one. Any changes are then sent to the browser.

let text = "Hello, world!"
page.add(() => ui.text(text))

text = "Hello, world! (updated)"
page.update() // Will re-run the callback function inside `page.add()`

Two key takeaways:

  • Update data immutably. For example, instead of mutating a single field in a table row, you should create a copy of the table data, mutate the copy, and reassign the original table data to the copy.
  • Avoid putting business logic (e.g. db queries, data transformations, etc.) inside page.add() or page.modal(). The business logic will re-run everytime page.update() is called. Instead, do this work outside of the callback function and simply pass the result into the page method.

Common pitfalls

Mutate data immutably

In order for page.update() to detect changes, all variable updates should be done immutably. For example, instead of mutating a single field in a table row, you should create a copy of the table data, mutate the copy, and reassign the original table data to the copy.

import { Compose, Page, UI } from "@composehq/sdk"

function handler({ page, ui }: { page: Page, ui: UI }) {
let tableData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
];

function onClick() {
// This won't work
tableData[1].name = "Jim"

// Do this instead
const copy = [...tableData]
copy[1].name = "Jim"
tableData = copy

page.update()
}

page.add(() => ui.stack([
ui.table("users", tableData),
ui.button("edit-table", {
onClick
}),
]))
}

const app = new Compose.App({ route: "table-update", handler })

Put business logic outside of page.add()

Putting business logic (e.g. db queries, data transformations, etc.) inside page.add() or page.modal() will cause the business logic to re-run everytime page.update() is called.

Instead, do this work outside of the callback function and simply pass the result into the page method.

import { Compose, Page, UI } from "@composehq/sdk"
import { fetchFromS3 } from "./utils"

function handler({ page, ui }: { page: Page, ui: UI }) {
// ❌ Since the S3 fetch is inside the callback function,
// it will be rerun everytime `page.update()` is called!
page.add(() => ui.pdf(fetchFromS3()))

// ✅ Instead, do this work outside of the callback function
const pdf = fetchFromS3()
page.add(() => ui.pdf(pdf))

// ... rest of the code
}

const app = new Compose.App({ route: "/pdf-app", handler })

Use stable component IDs

The SDK needs to be smart enough to not reset all your form and input values everytime you call page.update(). It accomplishes this by requiring all interactive components to have a stable, unique ID as their first argument. The unique ID is crucial for Compose to properly track the component and its state across updates.

Many bugs occur because of unstable component IDs such as list indices. For example:

let players = ["John", "Jane", "Jim"]

page.add(() => ui.form(
"player-names",
ui.forEach(players, (player, index) =>
ui.textInput(
`player-${index}`, // ❌ using unstable list index as component ID
{
initialValue: player,
label: "Player Name"
}
)
)
))

players.shift() // Remove the first player
page.update() // Update the form to remove the first player

The above code has a bug. In the first render, the form has the following input IDs:

  • player-0
  • player-1
  • player-2

After we remove the first player, the form will have the following input IDs:

  • player-0
  • player-1

From Compose's perspective, we've actually removed the last input component. To fix this, always use a stable, unique ID for components!

API reference

Function signature

page.update(): void

Parameters

None.

Returns

void