Skip to content

Comments

Fix: Skip component migrate/diff if fn differs#6181

Merged
FeodorFitsner merged 1 commit intomainfrom
hook-fix
Feb 18, 2026
Merged

Fix: Skip component migrate/diff if fn differs#6181
FeodorFitsner merged 1 commit intomainfrom
hook-fix

Conversation

@FeodorFitsner
Copy link
Contributor

@FeodorFitsner FeodorFitsner commented Feb 17, 2026

Prevent migrating or diffing Component instances when their underlying component function (fn) differs. Component._migrate_state now returns early if self.fn is not other.fn to avoid mixing incompatible positional hooks, and DiffBuilder was updated to require identical fn for treating component controls as the same (otherwise an item replace is emitted to force remount). Added a test to assert components are replaced when their fn changes.

This should work when PR is merged:

from dataclasses import dataclass

import flet as ft


@dataclass(frozen=True)
class ThemeContextValue:
    mode: ft.ThemeMode
    seed_color: ft.Colors


ThemeContext = ft.create_context(
    ThemeContextValue(
        mode=ft.ThemeMode.LIGHT,
        seed_color=ft.Colors.BLUE,
    )
)


@ft.component
def ContextFirst() -> ft.Control:
    # First hook is use_context().
    _ = ft.use_context(ThemeContext)
    return ft.Text("ContextFirst (first hook: use_context)")


@ft.component
def StateFirst() -> ft.Control:
    # First hook is use_state().
    value, set_value = ft.use_state("initial")
    return ft.Column(
        controls=[
            ft.Text(f"StateFirst (first hook: use_state), value={value}"),
            ft.TextButton("Update state", on_click=lambda _: set_value("updated")),
        ]
    )


@ft.component
def BrokenSwitch() -> ft.Control:
    show_state, set_show_state = ft.use_state(False)
    current = StateFirst() if show_state else ContextFirst()
    return ft.Column(
        controls=[
            ft.Text("Broken switch (no keys): toggling can crash with hook mismatch."),
            ft.ElevatedButton(
                "Toggle component",
                on_click=lambda _: set_show_state(not show_state),
            ),
            # Same slot, different component types with different first hook kinds.
            current,
        ]
    )


@ft.component
def FixedSwitch() -> ft.Control:
    show_state, set_show_state = ft.use_state(False)
    current = (
        ft.Container(key="state-first", content=StateFirst())
        if show_state
        else ft.Container(key="context-first", content=ContextFirst())
    )
    return ft.Column(
        controls=[
            ft.Text("Fixed switch (keyed remount): no hook slot reuse."),
            ft.ElevatedButton(
                "Toggle component",
                on_click=lambda _: set_show_state(not show_state),
            ),
            current,
        ]
    )


@ft.component
def App():
    return ft.Column(
        [
            ft.Text("Minimal repro for component identity/hook slot mismatch"),
            ft.Divider(),
            BrokenSwitch(),
            ft.Divider(),
            FixedSwitch(),
        ]
    )


if __name__ == "__main__":
    ft.run(lambda page: page.render(App))

Summary by Sourcery

Ensure component instances are only diffed and have state migrated when they originate from the same component function, forcing remounts otherwise to avoid hook mismatches.

Bug Fixes:

  • Avoid invalid hook state migration by skipping Component._migrate_state when the source and target component functions differ.
  • Prevent diffing of component controls with different component functions by treating them as replacements, ensuring correct remount behavior.

Tests:

  • Add a frozen-object diff test verifying that component list items are replaced, not diffed, when their component function changes.

Prevent migrating or diffing Component instances when their underlying component function (fn) differs. Component._migrate_state now returns early if self.fn is not other.fn to avoid mixing incompatible positional hooks, and DiffBuilder was updated to require identical fn for treating component controls as the same (otherwise an item replace is emitted to force remount). Added a test to assert components are replaced when their fn changes.
@cloudflare-workers-and-pages
Copy link

Deploying flet-examples with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4ee3708
Status: ✅  Deploy successful!
Preview URL: https://b9d20ab1.flet-examples.pages.dev
Branch Preview URL: https://hook-fix.flet-examples.pages.dev

View logs

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

@cloudflare-workers-and-pages
Copy link

Deploying flet-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4ee3708
Status: ✅  Deploy successful!
Preview URL: https://e0fce2ed.flet-docs.pages.dev
Branch Preview URL: https://hook-fix.flet-docs.pages.dev

View logs

@FeodorFitsner FeodorFitsner merged commit e23c144 into main Feb 18, 2026
57 of 93 checks passed
@FeodorFitsner FeodorFitsner deleted the hook-fix branch February 18, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant