reflex-dev / reflex

🕸️ Web apps in pure Python 🐍
https://reflex.dev
Apache License 2.0
19.58k stars 1.12k forks source link

Cannot render states with typing.ForwardRef #3437

Open guidocalvano opened 4 months ago

guidocalvano commented 4 months ago

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:

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 commented 4 months ago

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 commented 4 months ago

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

picklelo commented 4 months ago

@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.

shauryaryan commented 1 month ago

Can I work on this issue?

picklelo commented 1 month ago

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