Responding to Events#

ReactPy lets you add event handlers to your parts of the interface. These events handlers are functions which can be assigned to a part of a UI such that, when a user iteracts with the interface, those functions get triggered. Examples of interaction include clicking, hovering, of focusing on form inputs, and more.

Adding Event Handlers#

To start out we’ll just display a button that, for the moment, doesn’t do anything:

from reactpy import component, html, run


@component
def Button():
    return html.button("I don't do anything yet")


run(Button)

To add an event handler to this button we’ll do three things:

  1. Declare a function called handle_event(event) inside the body of our Button component

  2. Add logic to handle_event that will print the event it receives to the console.

  3. Add an "onClick": handle_event attribute to the <button> element.

from reactpy import component, html, run


@component
def Button():
    def handle_event(event):
        print(event)

    return html.button({"on_click": handle_event}, "Click me!")


run(Button)

Note

Normally print statements will only be displayed in the terminal where you launched ReactPy.

It may feel weird to define a function within a function like this, but doing so allows the handle_event function to access information from within the scope of the component. That’s important if you want to use any arguments that may have been passed your component in the handler:

from reactpy import component, html, run


@component
def PrintButton(display_text, message_text):
    def handle_event(event):
        print(message_text)

    return html.button({"on_click": handle_event}, display_text)


@component
def App():
    return html.div(
        PrintButton("Play", "Playing"),
        PrintButton("Pause", "Paused"),
    )


run(App)

With all that said, since our handle_event function isn’t doing that much work, if we wanted to streamline our component definition, we could pass in our event handler as a lambda:

html.button({"onClick": lambda event: print(message_text)}, "Click me!")

Supported Event Types#

Since ReactPy’s event information comes from React, most the the information (with some exceptions) about how React handles events translates directly to ReactPy. Follow the links below to learn about each category of event:

Passing Handlers to Components#

A common pattern when factoring out common logic is to pass event handlers into a more generic component definition. This allows the component to focus on the things which are common while still giving its usages customizablity. Consider the case below where we want to create a generic Button component that can be used for a variety of purpose:

from reactpy import component, html, run


@component
def Button(display_text, on_click):
    return html.button({"on_click": on_click}, display_text)


@component
def PlayButton(movie_name):
    def handle_click(event):
        print(f"Playing {movie_name}")

    return Button(f"Play {movie_name}", on_click=handle_click)


@component
def FastForwardButton():
    def handle_click(event):
        print("Skipping ahead")

    return Button("Fast forward", on_click=handle_click)


@component
def App():
    return html.div(
        PlayButton("Buena Vista Social Club"),
        FastForwardButton(),
    )


run(App)

Async Event Handlers#

Sometimes event handlers need to execute asynchronous tasks when they are triggered. Behind the scenes, ReactPy is running an asyncio event loop for just this purpose. By defining your event handler as an asynchronous function instead of a normal synchronous one. In the layout below we sleep for several seconds before printing out a message in the first button. However, because the event handler is asynchronous, the handler for the second button is still able to respond:

import asyncio

from reactpy import component, html, run


@component
def ButtonWithDelay(message, delay):
    async def handle_event(event):
        await asyncio.sleep(delay)
        print(message)

    return html.button({"on_click": handle_event}, message)


@component
def App():
    return html.div(
        ButtonWithDelay("print 3 seconds later", delay=3),
        ButtonWithDelay("print immediately", delay=0),
    )


run(App)

Event Data Serialization#

Not all event data is serialized. The most notable example of this is the lack of a target key in the dictionary sent back to the handler. Instead, data which is not inherently JSON serializable must be treated on a case-by-case basis. A simple case to demonstrate this is the currentTime attribute of audio and video elements. Normally this would be accessible via event.target.currentTime, but here it’s simply passed in under the key currentTime:

import json

import reactpy


@reactpy.component
def PlayDinosaurSound():
    event, set_event = reactpy.hooks.use_state(None)
    return reactpy.html.div(
        reactpy.html.audio(
            {
                "controls": True,
                "on_time_update": lambda e: set_event(e),
                "src": "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
            }
        ),
        reactpy.html.pre(json.dumps(event, indent=2)),
    )


reactpy.run(PlayDinosaurSound)

Client-side Event Behavior#

Because ReactPy operates server-side, there are inevitable limitations that prevent it from achieving perfect parity with all the behaviors of React. With that said, any feature that cannot be achieved in Python with ReactPy, can be done by creating Custom Javascript Components.

Preventing Default Event Actions#

Instead of calling an event.preventDefault() method as you would do in React, you must declare whether to prevent default behavior ahead of time. This can be accomplished using the event() decorator and setting prevent_default. For example, we can stop a link from going to the specified URL:

from reactpy import component, event, html, run


@component
def DoNotChangePages():
    return html.div(
        html.p("Normally clicking this link would take you to a new page"),
        html.a(
            {
                "on_click": event(lambda event: None, prevent_default=True),
                "href": "https://google.com",
            },
            "https://google.com",
        ),
    )


run(DoNotChangePages)

Unfortunately this means you cannot conditionally prevent default behavior in response to event data without writing Custom Javascript Components.

Stop Event Propagation#

Similarly to preventing default behavior, you can use the event() decorator to prevent events originating in a child element from propagating to parent elements by setting stop_propagation. In the example below we place a red div inside a parent blue div. When propagation is turned on, clicking the red element will cause the handler for the outer blue one to trigger. Conversely, when it’s off, only the handler for the red element will trigger.

from reactpy import component, event, hooks, html, run


@component
def DivInDiv():
    stop_propagatation, set_stop_propagatation = hooks.use_state(True)
    inner_count, set_inner_count = hooks.use_state(0)
    outer_count, set_outer_count = hooks.use_state(0)

    div_in_div = html.div(
        {
            "on_click": lambda event: set_outer_count(outer_count + 1),
            "style": {"height": "100px", "width": "100px", "background_color": "red"},
        },
        html.div(
            {
                "on_click": event(
                    lambda event: set_inner_count(inner_count + 1),
                    stop_propagation=stop_propagatation,
                ),
                "style": {
                    "height": "50px",
                    "width": "50px",
                    "background_color": "blue",
                },
            }
        ),
    )

    return html.div(
        html.button(
            {"on_click": lambda event: set_stop_propagatation(not stop_propagatation)},
            "Toggle Propagation",
        ),
        html.pre(f"Will propagate: {not stop_propagatation}"),
        html.pre(f"Inner click count: {inner_count}"),
        html.pre(f"Outer click count: {outer_count}"),
        div_in_div,
    )


run(DivInDiv)