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 |
---|---|
You want to quickly experiment with ReactPy and the Javascript ecosystem. |
|
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 theHTMLElement
thatrender()
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 givencomponent
.children
is an array of elements which were constructed by recursively callingcreate
.
The interface returned by bind()
can be thought of as being similar to that of
React.
create
➜React.createElement
render
➜ReactDOM.render
unmount
➜ReactDOM.unmountComponentAtNode
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>`;
}