reflex-dev / reflex

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

[REF-1501] Some proposals for call_script() #2301

Open yunge opened 9 months ago

yunge commented 9 months ago

The current version of call_script() is too limited, as it can only defines a State.callback_function, we can't use it directly in a function.

Suppose we have the following requirement, it's hard to implement with call_script(). (It's just an improvised idea, so please ignore its practical value.)

def update_items(self):
    # Get the width of a DOM from the frontend.
    container_width = rx.call_script_v2("document.body.getBoundingClientRect().width")

    # Use the obtained container width to determine how much data to display.
    item_num = 30
    if container_width < 1280:
        item_num=20

I tried to make an easier-to-use function, temporarily called run_script():

import asyncio
import time
from reflex import constants
from reflex.state import StateUpdate
from reflex.event import Event, EventHandler
from reflex.utils import prerequisites

async def run_script(state, javascript_code: str, callback):
    # create temporary handler function
    async def handler(state, v):
        # print(f'run_script() get value: {v}')
        callback(v)

    func_name = f"call_script_{time.time_ns()}"

    # add to event_handlers, use temporary name
    state.event_handlers[func_name] = EventHandler(fn=handler)
    # print(f'call_script state name: {state.get_full_name()}: {state.event_handlers}')

    # generate frontend event
    app = getattr(prerequisites.get_app(), constants.CompileVars.APP)
    events = [Event(
        token=state.router.session.client_token,
        name='_call_script',
        router_data={},
        payload={
            'javascript_code': javascript_code,
            'callback': 
            f'''(_eval_result) => queueEvents([Event("{state.get_full_name()}.{func_name}", {{v:_eval_result}})], socket)'''
        }
    )]

    await app.event_namespace.emit_update(
        update=StateUpdate(events=events),
        sid=state.router.session.session_id,
    )

Usage:

class TestState(BaseState):
    async def run_script_test(self):
        def callback(data):
            print(f'run_script callback> data: {data}')
        await run_script(self, f"document.body.getBoundingClientRect().width", callback)

(This is an unfinished version, I haven't been able to make it return a value directly, so it needs a callback instead, and no doubt the dev team can make a better version.)

I sincerely hope the Reflex team can consider these proposals, thanks.

REF-1501

masenf commented 9 months ago

Interesting approach, appreciate the sample code and good description.

I think this could work, the main difference to make run_script return the value is to have the internal callback function set the result of an asyncio.Future that the outer function awaits after emitting the update. As long as run_script is only ever called from a state event handler, then it should work.

A few other cleanups are necessary, like removing the temporary handler from event_handlers dict, but i don't immediately see anything that's a deal breaker from a reflex architecture perspective. I would probably implement the logic as rx.State.call_script so everything stays within rx.State.

yunge commented 9 months ago

rx.State.call_script would be great, thanks for your hard work.