Cannot render states with typing.ForwardRef
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...
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.
Using rx.cond also doesn't fix this...
@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.
Can I work on this issue?
@shauryaryan just assigned you to it - this one may be a bit hard, let us know if you need any help going through it