reflex icon indicating copy to clipboard operation
reflex copied to clipboard

Cannot render states with typing.ForwardRef

Open guidocalvano opened this issue 1 year ago β€’ 5 comments

Describe the bug I tried to render a tree, but kept getting missing attribute errors whenever I had a ForwardRef in my state.

To Reproduce Steps to reproduce the behavior:

  • Code/Link to Repo:

class ASTNodeState(rx.Base):
    key: str = "unknown"
    type: str = "unknown"
    text: str = "loading"
    children: List["ASTNodeState"] = []


class ASTNodeView(rx.ComponentState):

    @classmethod
    def get_component(cls, children: List[ASTNodeState], key: str, type: str, text: str) -> rx.Component:

        return rx.chakra.span(rx.text(key), rx.chakra.text(type), rx.chakra.text(text),
                              rx.foreach(children, render_child))

ast_node_view = ASTNodeView.create

def render_child(item: ASTNodeState):
    # hack to make recursion possible, this leads to an infinite loop in some deep copy function
    # item.__var_type = ASTNodeState
    # hack that I tried to use to derive type of the member (by also hacking in the Var.__getattr__ function)
    # item.___actual_class = ASTNodeState
    # final hack I tried, that also didn't work
    # other_item: ASTNodeState = item
    return ast_node_view(key=item.key, type=item.type, text=item.text, children=item.children)


Expected behavior I expected a beautiful tree structure to render...

Specifics (please complete the following information):

  • Python Version: 3.11 and 3.9 I tried
  • Reflex Version: reflex==0.5.2
  • OS: Ubuntu
  • Browser (Optional): Chrome, but the problem seems to occur in the python code.

Additional context When I hack the __var_type to the correct class I get infinite loops. I tried to create a member with the actual class and use that to detect type, but that for some weird reason raised an exception when run (but not when evaluated in my debugger).

The end of a frustrating day...

Weird stuff...

guidocalvano avatar Jun 04 '24 18:06 guidocalvano

Note that this means no cyclic references between states and thus no recursion. So you wouldn't be able to make redit, because redit contains a tree structure, which is recursive.

guidocalvano avatar Jun 05 '24 07:06 guidocalvano

Using rx.cond also doesn't fix this...

guidocalvano avatar Jun 05 '24 13:06 guidocalvano

@guidocalvano I was looking a bit into this yesterday. There's a couple fixes we need to make to the framework to fully support this, but the code below avoids the infinite recursion:

import reflex as rx
from typing import List


class ASTNodeState(rx.Base):
    key: str = "unknown"
    type: str = "unknown"
    text: str = "loading"
    children: List["ASTNodeState"] = []


class ASTNodeView(rx.ComponentState):

    @classmethod
    def get_component(
        cls, children: List[ASTNodeState], key: str, type: str, text: str
    ) -> rx.Component:
        # need to convert it to a var and specify the type.
        children = rx.Var.create(children).to(list[ASTNodeState])

        return rx.chakra.span(
            rx.text(key),
            rx.chakra.text(type),
            rx.chakra.text(text),
            rx.foreach(children, render_child),
        )


ast_node_view = ASTNodeView.create

# rx.memo pulls the function into it's own definition, avoiding the infinite recursion
@rx.memo
def ast_node(children: List[ASTNodeState], key: str, type: str, text: str):
    return ast_node_view(key=key, type=type, text=text, children=children)

def render_child(item: ASTNodeState):
    return ast_node(
        key=item.key, type=item.type, text=item.text, children=item.children
    )

def index() -> rx.Component:
    return rx.fragment(
        ast_node_view([
            ASTNodeState(key="1", type="type1", text="text1", children=[
                ASTNodeState(key="1.1", type="type1", text="text1", children=[
            ]),
            ]),
        ], key="root", type="root", text="root"),
    )


app = rx.App()
app.add_page(index)

The recursion is caused because by default Reflex evaluates all the components into one giant component for the page. Using @rx.memo pulls out the component into its own function instead of evaluating it. There's one bug we need to fix #3438 to fix the serialization and then this should work.

The @rx.memo isn't documented much because it was kind of a hack we needed. We're planning on cleaning it up and documenting it in the future.

picklelo avatar Jun 05 '24 16:06 picklelo

Can I work on this issue?

shauryaryan avatar Aug 12 '24 20:08 shauryaryan

@shauryaryan just assigned you to it - this one may be a bit hard, let us know if you need any help going through it

picklelo avatar Aug 13 '24 02:08 picklelo