Skip to content

Compile-time detection of @component functions with inconsistent state #37

@ntjess

Description

@ntjess

Consider the following simple component:

import solara as sl

@sl.component
def Page():
    initial = sl.use_reactive(1)

    def make_invalid():
        initial.set(-initial.value)

    sl.Button("Toggle invalid", on_click=make_invalid)

    if initial.value < 0:
        sl.Markdown("Invalid value")
        return
    another_reactive = sl.use_reactive(0)

After clicking the button, we get the following error:

Traceback (most recent call last):
  File "/Users/ntjess/miniconda3/envs/py312/lib/python3.12/site-packages/reacton/core.py", line 1751, in _render
    raise RuntimeError(
RuntimeError: Previously render had 4 effects, this run 3 (in element/component: Page()/react.component(__main__.Page)). Are you using conditional hooks?

In this case, it is clear how to fix the issue. But if the condition to trigger conditional hooks rarely appears, debugging is especially difficult.

I wrote the following function which attempts to detect these cases at compile time:

Details
DEFAULT_USE_FUNCTIONS = (
    "use_state",
    "use_reactive",
    "use_thread",
    "use_task",
    "use_effect",
    "use_memo",
)
def error_on_early_return(component: t.Callable, use_functions=DEFAULT_USE_FUNCTIONS):
    nodes = list(ast.walk(ast.parse(inspect.getsource(component))))
    earliest_return_node: ast.Return | None = None
    latest_use_node: ast.expr | None = None
    latest_use_node_id = ""
    for node in nodes:
        if isinstance(node, ast.Return):
            if (
                earliest_return_node is None
                or node.lineno > earliest_return_node.lineno
            ):
                earliest_return_node = node
        elif isinstance(node, ast.Call):
            func = node.func
            if isinstance(func, ast.Call):
                # Nested function, it will appear in another node later
                continue
            if isinstance(func, ast.Name):
                id_ = func.id
            elif isinstance(func, ast.Attribute):
                id_ = func.attr
            else:
                raise ValueError(
                    f"Unexpected function node type: {func}, {func.lineno=}"
                )
            if id_ in use_functions and (
                latest_use_node is None or node.lineno > latest_use_node.lineno
            ):
                latest_use_node = node
                latest_use_node_id = id_
    if (
        earliest_return_node
        and latest_use_node
        and earliest_return_node.lineno <= latest_use_node.lineno
    ):
        raise ValueError(
            f"{component}: `{latest_use_node_id}` found on line {latest_use_node.lineno} despite early"
            f" return on line {earliest_return_node. lineno}"
        )

Running this on the sample component provided a much more helpful error (again, at compile time instead of after a conditional toggle!):

ValueError: <function Page at 0x100ebe200>: `use_reactive` found on line 13 despite early return on line 12

I am curious what other cases should be detected and whether you think this function can be adopted into reacton or solara more generally.

  • use_* inside an if or for block
  • return before use_*
  • use_* outside a render context (this would be a bit harder, but still possible by tracing ast.Call usage)

In my case, I wrapped solara.component to call this function first and found a few other conditional hook bugs in my existing components.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions