Javascript Components#

While ReactPy is a great tool for displaying HTML and responding to browser events with pure Python, there are other projects which already allow you to do this inside Jupyter Notebooks or in standard web apps. The real power of ReactPy comes from its ability to seamlessly leverage the existing Javascript ecosystem. This can be accomplished in different ways for different reasons:

Integration Method

Use Case

Dynamically Loaded Components

You want to quickly experiment with ReactPy and the Javascript ecosystem.

Custom Javascript Components

You want to create polished software that can be easily shared with others.

Dynamically Loaded Components#

Note

This method is not recommended in production systems - see Distributing Javascript for more info. Instead, it’s best used during exploratory phases of development.

ReactPy makes it easy to draft your code when you’re in the early stages of development by using a CDN to dynamically load Javascript packages on the fly. In this example we’ll be using the ubiquitous React-based UI framework Material UI.

from reactpy import component, run, web

mui = web.module_from_template(
    "react@^17.0.0",
    "@material-ui/core@4.12.4",
    fallback="⌛",
)
Button = web.export(mui, "Button")


@component
def HelloWorld():
    return Button({"color": "primary", "variant": "contained"}, "Hello World!")


run(HelloWorld)

So now that we can display a Material UI Button we probably want to make it do something. Thankfully there’s nothing new to learn here, you can pass event handlers to the button just as you did when getting started. Thus, all we need to do is add an onClick handler to the component:

import json

import reactpy

mui = reactpy.web.module_from_template(
    "react@^17.0.0",
    "@material-ui/core@4.12.4",
    fallback="⌛",
)
Button = reactpy.web.export(mui, "Button")


@reactpy.component
def ViewButtonEvents():
    event, set_event = reactpy.hooks.use_state(None)

    return reactpy.html.div(
        Button(
            {
                "color": "primary",
                "variant": "contained",
                "onClick": lambda event: set_event(event),
            },
            "Click Me!",
        ),
        reactpy.html.pre(json.dumps(event, indent=2)),
    )


reactpy.run(ViewButtonEvents)

Custom Javascript Components#

For projects that will be shared with others, we recommend bundling your Javascript with Rollup or Webpack into a web module. ReactPy also provides a template repository that can be used as a blueprint to build a library of React components.

To work as intended, the Javascript bundle must export a function bind() that adheres to the following interface:

type EventData = {
    target: string;
    data: Array<any>;
}

type LayoutContext = {
    sendEvent(data: EventData) => void;
    loadImportSource(source: string, sourceType: "NAME" | "URL") => Module;
}

type bind = (node: HTMLElement, context: LayoutContext) => ({
    create(type: any, props: Object, children: Array<any>): any;
    render(element): void;
    unmount(): void;
});

Note

  • node is the HTMLElement that render() should mount to.

  • context can send events back to the server and load “import sources” (like a custom component module).

  • type is a named export of the current module, or a string (e.g. "div", "button", etc.)

  • props is an object containing attributes and callbacks for the given component.

  • children is an array of elements which were constructed by recursively calling create.

The interface returned by bind() can be thought of as being similar to that of React.

It will be used in the following manner:

// once on mount
const binding = bind(node, context);

// on every render
let element = binding.create(type, props, children)
binding.render(element);

// once on unmount
binding.unmount();

The simplest way to try this out yourself though, is to hook in a simple hand-crafted Javascript module that has the requisite interface. In the example to follow we’ll create a very basic SVG line chart. The catch though is that we are limited to using Javascript that can run directly in the browser. This means we can’t use fancy syntax like JSX and instead will use htm to simulate JSX in plain Javascript.

from pathlib import Path

from reactpy import component, run, web

file = Path(__file__).parent / "super-simple-chart.js"
ssc = web.module_from_file("super-simple-chart", file, fallback="⌛")
SuperSimpleChart = web.export(ssc, "SuperSimpleChart")


@component
def App():
    return SuperSimpleChart(
        {
            "data": [
                {"x": 1, "y": 2},
                {"x": 2, "y": 4},
                {"x": 3, "y": 7},
                {"x": 4, "y": 3},
                {"x": 5, "y": 5},
                {"x": 6, "y": 9},
                {"x": 7, "y": 6},
            ],
            "height": 300,
            "width": 500,
            "color": "royalblue",
            "lineWidth": 4,
            "axisColor": "silver",
        }
    )


run(App)
import { h, render } from "https://unpkg.com/preact?module";
import htm from "https://unpkg.com/htm?module";

const html = htm.bind(h);

export function bind(node, config) {
  return {
    create: (component, props, children) => h(component, props, ...children),
    render: (element) => render(element, node),
    unmount: () => render(null, node),
  };
}

export function SuperSimpleChart(props) {
  const data = props.data;
  const lastDataIndex = data.length - 1;

  const options = {
    height: props.height || 100,
    width: props.width || 100,
    color: props.color || "blue",
    lineWidth: props.lineWidth || 2,
    axisColor: props.axisColor || "black",
  };

  const xData = data.map((point) => point.x);
  const yData = data.map((point) => point.y);

  const domain = {
    xMin: Math.min(...xData),
    xMax: Math.max(...xData),
    yMin: Math.min(...yData),
    yMax: Math.max(...yData),
  };

  return html`<svg
    width="${options.width}px"
    height="${options.height}px"
    viewBox="0 0 ${options.width} ${options.height}"
  >
    ${makePath(props, domain, data, options)} ${makeAxis(props, options)}
  </svg>`;
}

function makePath(props, domain, data, options) {
  const { xMin, xMax, yMin, yMax } = domain;
  const { width, height } = options;
  const getSvgX = (x) => ((x - xMin) / (xMax - xMin)) * width;
  const getSvgY = (y) => height - ((y - yMin) / (yMax - yMin)) * height;

  let pathD =
    `M ${getSvgX(data[0].x)} ${getSvgY(data[0].y)} ` +
    data.map(({ x, y }, i) => `L ${getSvgX(x)} ${getSvgY(y)}`).join(" ");

  return html`<path
    d="${pathD}"
    style=${{
      stroke: options.color,
      strokeWidth: options.lineWidth,
      fill: "none",
    }}
  />`;
}

function makeAxis(props, options) {
  return html`<g>
    <line
      x1="0"
      y1=${options.height}
      x2=${options.width}
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
    <line
      x1="0"
      y1="0"
      x2="0"
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
  </g>`;
}