Reactive Expressions#

In this section you will learn about pn.rx. pn.rx extends the concepts from pn.bind that your learned in the previous section.

Note

You might feel some repetition from the previous section on pn.bind. We do this on purpose to enable you to compare and contrast. pn.rx is the an extension of pn.bind that provides more power and flexibility. pn.bind has been the core API in Panel for a long time, so you will meet it across our documentation and community sites, and thus its very important to learn.

pn.rx will enable you to build more complicated applications using a more flexible and maintainable architecture.

Embrace pn.rx#

pn.rx allows you to treat any object as a reactive expression. This means we can do things like multiplying a widget (representing the wind speed) with a float value (representing the efficiency) and then format the result all without writing callbacks:

import panel as pn

pn.extension()

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)

efficiency = 0.3

power = wind_speed.rx() * efficiency

power_text = pn.rx(
    "Wind Speed: {wind_speed} m/s, "
    "Efficiency: {efficiency}, "
    "Power Generation: {power:.1f} kW"
).format(wind_speed=wind_speed, efficiency=efficiency, power=power)

pn.Column(power_text).servable()

You will notice how adding power_text to the Column displays both the widget and the bound function.

To separate the widget and bound function we can use the power_text as a reference when we construct a Markdown pane:

import panel as pn

pn.extension()

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)

efficiency = 0.3

power = wind_speed.rx() * efficiency

power_text = pn.rx(
    "Wind Speed: {wind_speed} m/s, "
    "Efficiency: {efficiency}, "
    "Power Generation: {power:.1f} kW"
).format(wind_speed=wind_speed, efficiency=efficiency, power=power)

power_md = pn.pane.Markdown(power_text)

pn.Column(wind_speed, power_md).servable()

You can of course write expressions with multiple widgets. Lets make the efficiency a widget:

import panel as pn

pn.extension()

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")

power = wind_speed.rx() * efficiency.rx()

power_text = pn.rx(
    "Wind Speed: {wind_speed} m/s, "
    "Efficiency: {efficiency}, "
    "Power Generation: {power:.1f} kW"
).format(wind_speed=wind_speed, efficiency=efficiency, power=power)

power_md = pn.pane.Markdown(power_text)

pn.Column(wind_speed, efficiency, power_md).servable()

Crafting Interactive Forms#

Forms are powerful tools for collecting user inputs. With .rx.when you can easily defer some calculation (i.e. the form submission) until some event (such as a button click) is triggered:

import panel as pn

pn.extension()

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = pn.widgets.FloatInput(
    value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))"
)
submit = pn.widgets.Button(name="Submit", button_type="primary")

power = wind_speed.rx() * efficiency.rx()

power_text = pn.rx(
    "Wind Speed: {wind_speed} m/s, "
    "Efficiency: {efficiency}, "
    "Power Generation: {power:.1f} kW"
).format(
    wind_speed=wind_speed,
    efficiency=efficiency,
    power=power
).rx.when(submit)

pn.Column(
    wind_speed, efficiency, submit, pn.pane.Markdown(power_text)
).servable()

Try changing some of the inputs and clicking the submit Button. Try again. Notice how the text is only updated when we click the submit Button - we used .rx.when to achieve this effect.

Harnessing Throttling for Performance#

To prevent excessive updates and ensure smoother performance, you can apply throttling (.value_throttled). This limits the rate at which certain actions or events occur, maintaining a balanced user experience:

import panel as pn

pn.extension()

wind_speed = pn.widgets.FloatSlider(
    value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
)
efficiency = 0.3

power = wind_speed.param.value_throttled.rx() * efficiency

power_text = pn.rx(
    "Wind Speed: {wind_speed} m/s, "
    "Efficiency: {efficiency}, "
    "Power Generation: {power:.1f} kW"
).format(
    wind_speed=wind_speed.param.value_throttled,
    efficiency=0.3,
    power=power
)

pn.Column(
    wind_speed, pn.pane.Markdown(power_text)
).servable()

Try dragging the slider. Notice that the text is only updated when you release the mouse.

Triggering Side Effects with .watch#

When you need to trigger additional tasks in response to user actions, using .watch comes in handy:

import panel as pn

pn.extension()

# Declare state of application
is_stopped = pn.rx(True)

rx_name = is_stopped.rx.where("Start the wind turbine", "Stop the wind turbine")

submit = pn.widgets.Button(name=rx_name)

def toggle_wind_turbine(clicked):
    is_stopped.rx.value = not is_stopped.rx.value

submit.rx.watch(toggle_wind_turbine)

pn.Column(submit).servable()

Here we store the state of the windturbine in a separate rx variable, whenever the submit button is clicked we toggle the state.

Tip

When building apps using functions we recommend this approach to storing state. Applications are easier to reason about when the application displays according to the state instead of being changed directly.

Keep the UI responsive with threads or processes#

To keep your UI and server responsive while the long running, blocking task is running you might want to run it asyncrounously in a separate thread:

import asyncio
import time

import panel as pn

pn.extension()

is_stopped = pn.rx(True)

submit = pn.widgets.Button(
    name=is_stopped.rx.where("Start the wind turbine", "Stop the wind turbine"),
)

async def start_stop_wind_turbine(clicked):
    with submit.param.update(loading=True, disabled=True):
        await asyncio.to_thread(time.sleep, 1)
        is_stopped.rx.value = not is_stopped.rx.value

submit.rx.watch(start_stop_wind_turbine)

pn.Column(submit).servable()

Note

In the example we use a asyncio.to_thread this should work great if your blocking task releases the GIL while running. Tasks that request data from the web or read data from files typically do this. Some computational methods from Numpy, Pandas etc. also release the GIL. If your long running task does not release the GIL you may have to use a ProcessPoolExecutor instead. This introduces some overhead though.

Recap#

You’ve now unlocked the power of interactivity in your Panel applications:

  • some_widget.rx(): for seamless updates based on widget values.

  • pn.rx(some_function)(widget_1, widget_2): for seamless updates based on widget values.

  • pn.rx(some_task, some_widget).rx.watch(): for triggering tasks in response to user actions.

  • Throttling ensures smoother performance by limiting update frequency.

  • Utilizing async and threading keeps your UI responsive during long-running tasks.

Now, let your imagination run wild and craft dynamic, engaging Panel applications!

Resources#

Reference Guides#

ReactiveExpr reference guide

How-to#

Explanation#

External#