Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component as Var type #3732

Merged
merged 38 commits into from
Sep 20, 2024
Merged

Component as Var type #3732

merged 38 commits into from
Sep 20, 2024

Conversation

masenf
Copy link
Collaborator

@masenf masenf commented Aug 1, 2024

@adhami3310 did most of the initial research for this one 👏

  • It depends on importing components from a CDN instead of getting them from the local webpack bundle
  • The component var must start with comp_ to be considered a dynamic component. In the future a special type of computed var will be used as the marker.
  • The returned JS code is imported using a dynamic data uri module... but this means the function that evals the module needs to be async so it can await the import. Because of this, we cannot have the component function in the initial state (yet).
  • Eventually the make_component and component_var decorator should become part of the framework instead of defined in the sample code.
  • Embedded css props are not properly converted because these require the babel emotion plugin... but that plugin does not seem compatible with the standalone babel as it tries to bring in the node.js fs module.

Sample code

import reflex as rx
from reflex.compiler import templates, utils


def make_component(component: rx.Component) -> str:

    rendered_components = {}
    # Include dynamic imports in the shared component.
    if dynamic_imports := component._get_all_dynamic_imports():
        rendered_components.update(
            {dynamic_import: None for dynamic_import in dynamic_imports}
        )

    # Include custom code in the shared component.
    rendered_components.update(
        {code: None for code in component._get_all_custom_code()},
    )

    rendered_components[
        templates.STATEFUL_COMPONENT.render(
            tag_name="MySSRComponent",
            memo_trigger_hooks=[],
            component=component,
        )
    ] = None

    imports = {}
    for lib, names in component._get_all_imports().items():
        if not lib.startswith((".", "/")) and not lib.startswith("http") and not lib == "react":
            imports[f"https://cdn.jsdelivr.net/npm/{lib}" + "/+esm"] = names
        else:
            imports[lib] = names

    module_code_lines = templates.STATEFUL_COMPONENTS.render(
        imports=utils.compile_imports(imports),
        memoized_code="\n".join(rendered_components),
    ).splitlines()[1:]

    # Rewrite imports from `/` to destructure from window
    for ix, line in enumerate(module_code_lines[:]):
        if line.startswith("import "): 
            if 'from "/' in line:
                module_code_lines[ix] = line.replace("import ", "const ", 1).replace(" from ", " = window[", 1)  + "]"
            elif 'from "react"' in line:
                module_code_lines[ix] = line.replace("import ", "const ", 1).replace(' from "react"', " = window.React", 1)
        if line.startswith("export function"):
            module_code_lines[ix] = line.replace("export function", "export default function", 1)
    return "\n".join(module_code_lines)


def component_var():
    def outer(fn):
        def inner(*args, **kwargs) -> str:
            return make_component(fn(*args, **kwargs))
        inner.__name__ = fn.__name__
        return rx.var(inner)
    return outer



class State(rx.State):
    count: int = 0

    def increment(self):
        self.count += 1

    @component_var()
    def comp_buttoned_count(self) -> rx.Component:
        return rx.button(
            rx.text("Hi Mom"),
            f"Click me {self.count}",
            on_click=type(self).increment,
        )


def index() -> rx.Component:
    return (
        rx.box(
            rx.button(
                "Click me",
                on_click=State.increment,
            ),
            rx.text(State.count),
            State.comp_buttoned_count,
        ),
    )


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

adhami3310 and others added 13 commits September 11, 2024 13:17
* override dict in propsbase to use camelCase

* fix underscore in dict

* dang it darglint
* half of the way there

* add dataclass support

* Forbid Computed var shadowing (#3843)

* get it right pyright

* fix unit tests

* rip out more pydantic

* fix weird issues with merge_imports

* add missing docstring

* make special props a list instead of a set

* fix moment pyi

* actually ignore the runtime error

* it's ruff out there

---------

Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com>
@adhami3310 adhami3310 changed the title [WiP] Support UI components returned from a computed var Component as Var type Sep 14, 2024
@adhami3310 adhami3310 marked this pull request as ready for review September 14, 2024 01:03
@adhami3310
Copy link
Member

import reflex as rx


class State(rx.State):
    component_string: str = "rx.button('hi')"

    message: rx.Component = rx.el.div("Hello")

    @rx.var
    def component(self) -> rx.Component:
        try:
            comp = eval(
                self.component_string,
                {
                    "rx": rx,
                    "State": State,
                },
            )
            if not isinstance(comp, rx.Component):
                return rx.el.div("Invalid component")
            return comp
        except Exception as e:
            return rx.el.div(str(e))

    def im_feeling_lucky(self):
        import random

        components = [
            rx.el.div("Hello"),
            rx.button("Click me", on_click=State.im_feeling_lucky),
            rx.text_area("Type here"),
            rx.badge("New"),
        ]

        self.message = random.choice(components)


def index():
    return rx.hstack(
        rx.vstack(
            rx.heading(
                "Input",
                size="3",
            ),
            rx.text_area(
                value=State.component_string,
                on_change=State.set_component_string,
                width="100%",
                flex="1",
                font_family="monospace",
            ),
            height="100%",
            flex="1",
        ),
        rx.vstack(
            rx.heading(
                "Output",
                size="3",
            ),
            rx.card(State.component, width="100%", flex="1"),
            rx.hstack(
                rx.button("I'm feeling lucky", on_click=State.im_feeling_lucky),
                State.message,
            ),
            height="100%",
            flex="1",
        ),
        height="100vh",
        width="100vw",
        box_sizing="border-box",
        padding="1rem",
        gap="1rem",
    )


app = rx.App()
app.add_page(index, "/")

@picklelo picklelo mentioned this pull request Sep 17, 2024
24 tasks
Copy link
Collaborator

@Lendemor Lendemor left a comment

Choose a reason for hiding this comment

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

Got a small question:
How does this impact the initial size for the frontend ?

import * as React from "react";
import * as utils_context from "/utils/context.js";
import * as utils_state from "/utils/state.js";
import * as radix from "@radix-ui/themes";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will this import all the radix components all the time?

Copy link
Member

Choose a reason for hiding this comment

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

Yes. In this PR we are not using radix that is stored in the window. I assume loading it there takes some additional time (sadly, not sure if there's any way of doing this dynamically).

Copy link
Contributor

@picklelo picklelo left a comment

Choose a reason for hiding this comment

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

Functionality is working well for me, this is going to be huge.

  • In a follow up we need to add more unit tests for all the new functions introduced here
  • [discussed offline] There's a limitation on returning vars from other vars (leading to a "bug in React" error) - we should try to catch that happening and prevent it

Once this is rebased it should be good to merge!

Copy link
Contributor

@picklelo picklelo left a comment

Choose a reason for hiding this comment

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

The first step of the new reflex!

@adhami3310 adhami3310 merged commit bca49d3 into main Sep 20, 2024
46 checks passed
@masenf masenf deleted the feat/dynamic-components branch December 12, 2024 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request A feature you wanted added to reflex
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants