# Imperative To Declarative Flet

> Convert an imperative Flet Python app in which controls are mutated and then page.update is called to declarative style using flet.component, flet.observable and state hooks.

- Skill: `flet-dev/imperative-to-declarative-flet` (Agent Skill)
- Install (CLI): `npx skillmds@latest add flet-dev/imperative-to-declarative-flet`
- Raw SKILL.md: https://api.skillmd.com/api/skills/flet-dev/imperative-to-declarative-flet/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: flet-dev (https://skillmd.com/u/flet-dev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/flet-dev/imperative-to-declarative-flet

---


# Imperative → Declarative (Flet Python)

Port an existing imperative Flet app to “components mode” with `@ft.component`, hooks, and (optionally) `@ft.observable` models.

## Outcomes

- Original folder unchanged; new sibling folder created.
- Entry point uses `page.render(App)` (components mode).
- UI is derived from state; minimal/zero manual `page.update()` for normal UI updates.

## Choose a state strategy

- **Use `@ft.observable`** for nested app data you mutate in place (boards/lists/cards, drag+drop reorder).
- **Use `ft.use_state`** for local ephemeral UI state (hover flags, input text, dialog selection).
- Avoid storing live `Control`/component objects in state; store ids/enums and create controls during render.

## Workflow

### 1) Create a new declarative copy

- Copy the existing app folder to a new one (e.g. `trolli` → `trolli-declarative-*`).
- Keep `assets/` with the new folder.

### 2) Switch to components mode entrypoint

- Create a root component `@ft.component def App(): ...`.
- Run with:
  - `ft.run(lambda page: page.render(App), assets_dir=...)`
- Set page globals either:
  - in a `main(page)` function before `page.render(App)`, or
  - in `ft.on_mounted(...)` (works, but ordering can be less obvious).

### 3) Centralize routing in `App`

- Keep `app.route: str` as source of truth.
- In one place, define:
  - `route_change(e)` to normalize/redirect/validate routes and set `app.route`
  - a render-time `match app.route` (or a derived `active_screen`) to pick `content`
- Prefer: `route_change` mutates route state; `App()` render chooses UI based on that state.

### 4) Componentize UI

- Move UI chunks into `src/components/*.py`.
- Each component:
  - takes only the model(s) it needs
  - uses hooks for local UI state
  - mutates observable models for app data changes

### 5) Dialogs: reduce `page.update()` usage

If a dialog mutates existing controls and calls `page.update()`, convert it to:

- `@ft.component` dialog content with `ft.use_state` for `error`, `selected_color`, etc.
- event handlers call setters (no explicit updates)
- show it via `page.show_dialog(ft.AlertDialog(content=DialogContent(...)))`

### 6) Assets/fonts checklist

- Prefer an absolute `assets_dir` derived from `__file__`.
- If running via `flet run`, be aware it can set `FLET_ASSETS_DIR` and override `assets_dir=`.
- Font registration uses the dict key as the font family name:
  - `page.fonts = {"Pacifico": "Pacifico-Regular.ttf"}`
  - use `font_family="Pacifico"`.

## Common pitfalls (and fixes)

- **Event handler typing is invariant** (`Event[Sub]` ≠ `Event[Base]`):
  - If `on_click` is declared on `Button`, annotate `e` as `ft.Event[ft.Button]` (or use `def handler(): ...`).
- **`TemplateRoute` params are dynamic**:
  - `raw = getattr(troute, "id", None)` then `isinstance(raw, str)` before `int(raw)`.
- **`controls=[*[...], ...]` can confuse type checkers** with components:
  - build lists in two steps and annotate/cast, or return raw controls from non-component factories.

