Reacton#

Open this notebook in Jupyterlite | Download this notebook from GitHub (right-click to download).


import numpy as np
import panel as pn
import reacton
import reacton.ipywidgets as w

pn.extension('ipywidgets')

The Reacton pane renders Reacton components both in the notebook and in a deployed server. Reacton provides a way to write reusable components in a React-like way, to make Python-based UI’s using the ipywidgets ecosystem (ipywidgets, ipyvolume, bqplot, threejs, leaflet, ipyvuetify, …). Note that Reacton is primarily a way to write apps

In the notebook this is not necessary since Panel simply uses the regular notebook ipywidget renderer. Particularly in JupyterLab importing the ipywidgets extension in this way may interfere with the UI and render the JupyterLab UI unusable, so enable the extension with care.

Parameters:#

For details on other options for customizing the component see the layout and styling how-to guides.

  • object (object): The ipywidget object being displayed

Display#

  • default_layout (pn.layout.Panel, default=Row): Layout to wrap the plot and widgets in


The panel function will automatically convert any ipywidgets object into a displayable panel, while keeping all of its interactive features:

@reacton.component
def ButtonClick():
    # first render, this return 0, after that, the last argument
    # of set_clicks
    clicks, set_clicks = reacton.use_state(0)
    
    def my_click_handler():
        # trigger a new render with a new value for clicks
        set_clicks(clicks+1)

    button = w.Button(description=f"Clicked {clicks} times",
                      on_click=my_click_handler)
    return button

pn.panel(ButtonClick())

Combining Reacton and Panel components#

Reacton can be used in conjunction with Panel components however we have to make two modifications:

  1. Panel components have to be wrapped as an ipywidget using the pn.ipywidget wrapper (this requires jupyter_bokeh).

  2. The wrapped Panel component must be added to a reacton layout component.

In the example below we swap out the reacton.ipywidgets.Button for a pn.widgets.Button and then wrap it in pn.ipywidgets and a reacton.ipywidgets.VBox:

@reacton.component
def PanelButtonClick():
    # first render, this return 0, after that, the last argument
    # of set_clicks
    clicks, set_clicks = reacton.use_state(0)
    
    def my_click_handler(event):
        # trigger a new render with a new value for clicks
        set_clicks(clicks+1)

    button = pn.widgets.Button(name=f'Clicked {clicks} times')
    button.on_click(my_click_handler)

    return w.VBox(children=[pn.ipywidget(button)])

pn.panel(PanelButtonClick(), height=50)

Complex examples#

Even more complex applications can be built in Reacton and displayed in Panel. Here is a Calculator example from the Reacton documentation.

Logic#

import ast
import dataclasses
import operator
from typing import Any, Optional

DEBUG = False
operator_map = {
    "x": operator.mul,
    "/": operator.truediv,
    "+": operator.add,
    "-": operator.sub,
}

@dataclasses.dataclass(frozen=True)
class CalculatorState:
    input: str = ""
    output: str = ""
    left: float = 0
    right: Optional[float] = None
    operator: Any = operator.add
    error: str = ""


initial_state = CalculatorState()


def calculate(state: CalculatorState):
    result = state.operator(state.left, state.right)
    return dataclasses.replace(state, left=result)


def calculator_reducer(state: CalculatorState, action):
    action_type, payload = action
    if DEBUG:
        print("reducer", state, action_type, payload)  # noqa
    state = dataclasses.replace(state, error="")

    if action_type == "digit":
        digit = payload
        input = state.input + digit
        return dataclasses.replace(state, input=input, output=input)
    elif action_type == "percent":
        if state.input:
            try:
                value = ast.literal_eval(state.input)
            except Exception as e:
                return dataclasses.replace(state, error=str(e))
            state = dataclasses.replace(state, right=value / 100)
            state = calculate(state)
            output = f"{value / 100:,}"
            return dataclasses.replace(state, output=output, input="")
        else:
            output = f"{state.left / 100:,}"
            return dataclasses.replace(state, left=state.left / 100, output=output)
    elif action_type == "negate":
        if state.input:
            input = state.output
            input = input[1:] if input[0] == "-" else "-" + input
            output = input
            return dataclasses.replace(state, input=input, output=output)
        else:
            output = f"{-state.left:,}"
            return dataclasses.replace(state, left=-state.left, output=output)
    elif action_type == "clear":
        return dataclasses.replace(state, input="", output="")
    elif action_type == "reset":
        return initial_state
    elif action_type == "calculate":
        if state.input:
            try:
                value = ast.literal_eval(state.input)
            except Exception as e:
                return dataclasses.replace(state, error=str(e))
            state = dataclasses.replace(state, right=value)
        state = calculate(state)
        output = f"{state.left:,}"
        state = dataclasses.replace(state, output=output, input="")
        return state
    elif action_type == "operator":
        if state.input:
            state = calculator_reducer(state, ("calculate", None))
            state = dataclasses.replace(state, operator=payload, input="")
        else:
            # e.g. 2+3=*= should give 5,25
            state = dataclasses.replace(state, operator=payload, right=state.left)
        return state
    else:
        print("invalid action", action)  # noqa
        return state

UI#

ipywidgets#

@reacton.component
def Calculator():
    state, dispatch = reacton.use_reducer(calculator_reducer, initial_state)
    with w.VBox() as main:
        w.HTML(value="<b>Calculator Using Reacton</b>")
        with w.VBox():
            w.HTML(value=state.error or state.output or "0")
            with w.HBox():
                if state.input:
                    w.Button(description="C", on_click=lambda: dispatch(("clear", None)))
                else:
                    w.Button(description="AC", on_click=lambda: dispatch(("reset", None)))
                w.Button(description="+/-", on_click=lambda: dispatch(("negate", None)))
                w.Button(description="%", on_click=lambda: dispatch(("percent", None)))
                w.Button(description="/", on_click=lambda: dispatch(("operator", operator_map["/"])))

            column_op = ["x", "-", "+"]
            for i in range(3):
                with w.HBox():
                    for j in range(3):
                        digit = str(j + (2 - i) * 3 + 1)
                        w.Button(description=digit, on_click=lambda digit=digit: dispatch(("digit", digit)))
                    op_symbol = column_op[i]
                    op = operator_map[op_symbol]
                    w.Button(description=op_symbol, on_click=lambda op=op: dispatch(("operator", op)))
            with w.HBox():
                def boom():
                    print("boom")
                    raise ValueError("boom")

                w.Button(description="?", on_click=boom)

                w.Button(description="0", on_click=lambda: dispatch(("digit", "0")))
                w.Button(description=".", on_click=lambda: dispatch(("digit", ".")))

                w.Button(description="=", on_click=lambda: dispatch(("calculate", None)))

    return main

calculator = Calculator()
    
pn.pane.Reacton(calculator, width=500, height=250)

ipyvuetify#

import reacton.ipyvuetify as v

@reacton.component
def CalculatorVuetify():
    state, dispatch = reacton.use_reducer(calculator_reducer, initial_state)
    with v.Card(elevation=10, class_="ma-4") as main:
        with v.CardTitle(children=["Calculator"]):
            pass
        with v.CardSubtitle(children=["With ipyvuetify and Reacton"]):
            pass
        with v.CardText():
            with w.VBox():
                w.HTML(value=state.error or state.output or "0")
                class_ = "pa-0 ma-1"

                with w.HBox():
                    if state.input:
                        v.BtnWithClick(children="C", on_click=lambda: dispatch(("clear", None)), dark=True, class_=class_)
                    else:
                        v.BtnWithClick(children="AC", on_click=lambda: dispatch(("reset", None)), dark=True, class_=class_)
                    v.BtnWithClick(children="+/-", on_click=lambda: dispatch(("negate", None)), dark=True, class_=class_)
                    v.BtnWithClick(children="%", on_click=lambda: dispatch(("percent", None)), dark=True, class_=class_)
                    v.BtnWithClick(children="/", color="primary", on_click=lambda: dispatch(("operator", operator_map["/"])), class_=class_)

                column_op = ["x", "-", "+"]
                for i in range(3):
                    with w.HBox():
                        for j in range(3):
                            digit = str(j + (2 - i) * 3 + 1)
                            v.BtnWithClick(children=digit, on_click=lambda digit=digit: dispatch(("digit", digit)), class_=class_)
                        op_symbol = column_op[i]
                        op = operator_map[op_symbol]
                        v.BtnWithClick(children=op_symbol, color="primary", on_click=lambda op=op: dispatch(("operator", op)), class_=class_)
                with w.HBox():
                    def boom():
                        print("boom")
                        raise ValueError("boom")

                    v.BtnWithClick(children="?", on_click=boom, class_=class_)

                    v.BtnWithClick(children="0", on_click=lambda: dispatch(("digit", "0")), class_=class_)
                    v.BtnWithClick(children=".", on_click=lambda: dispatch(("digit", ".")), class_=class_)

                    v.BtnWithClick(children="=", color="primary", on_click=lambda: dispatch(("calculate", None)), class_=class_)

    return main

pn.pane.Reacton(CalculatorVuetify(), width=500, height=420)

Open this notebook in Jupyterlite | Download this notebook from GitHub (right-click to download).