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:
typeEventData={ target:string; data:Array<any>; } typeLayoutContext={ sendEvent(data:EventData)=>void; loadImportSource(source:string,sourceType:"NAME"|"URL")=>Module; } typebind=(node:HTMLElement,context:LayoutContext)=>({ create(type:any,props:Object,children:Array<any>):any; render(element):void; unmount():void; });
Note
nodeis theHTMLElementthatrender()should mount to.contextcan send events back to the server and load "import sources" (like a custom component module).typeis a named export of the current module, or a string (e.g."div","button", etc.)propsis an object containing attributes and callbacks for the givencomponent.childrenis 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.createElementrenderβReactDOM.renderunmountβReactDOM.unmountComponentAtNode
It will be used in the following manner:
// once on mount constbinding=bind(node,context); // on every render letelement=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"; importhtmfrom"https://unpkg.com/htm?module"; consthtml=htm.bind(h); exportfunctionbind(node,config){ return{ create:(component,props,children)=>h(component,props,...children), render:(element)=>render(element,node), unmount:()=>render(null,node), }; } exportfunctionSuperSimpleChart(props){ constdata=props.data; constlastDataIndex=data.length-1; constoptions={ height:props.height||100, width:props.width||100, color:props.color||"blue", lineWidth:props.lineWidth||2, axisColor:props.axisColor||"black", }; constxData=data.map((point)=>point.x); constyData=data.map((point)=>point.y); constdomain={ xMin:Math.min(...xData), xMax:Math.max(...xData), yMin:Math.min(...yData), yMax:Math.max(...yData), }; returnhtml`<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>`; } functionmakePath(props,domain,data,options){ const{xMin,xMax,yMin,yMax}=domain; const{width,height}=options; constgetSvgX=(x)=>((x-xMin)/(xMax-xMin))*width; constgetSvgY=(y)=>height-((y-yMin)/(yMax-yMin))*height; letpathD= `M ${getSvgX(data[0].x)}${getSvgY(data[0].y)} `+ data.map(({x,y},i)=>`L ${getSvgX(x)}${getSvgY(y)}`).join(" "); returnhtml`<path d="${pathD}" style=${{ stroke:options.color, strokeWidth:options.lineWidth, fill:"none", }} />`; } functionmakeAxis(props,options){ returnhtml`<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>`; }