{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "50cd605c-3e41-4909-9214-b0aafd171d25",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"id": "70eb493f-4216-4d8c-b763-f98b67933cec",
"metadata": {},
"source": [
"`ReactComponent` simplifies the creation of custom Panel components by allowing you to write standard [React](https://react.dev/) code without the need to pre-compile or requiring a deep understanding of Javascript build tooling."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ce37ac2a-1fea-4e45-8581-b913e0e05097",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"import param\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"\n",
"class CounterButton(ReactComponent):\n",
"\n",
" value = param.Integer()\n",
"\n",
" _esm = \"\"\"\n",
" export function render({model}) {\n",
" const [value, setValue] = model.useState(\"value\");\n",
" return (\n",
" \n",
" )\n",
" }\n",
" \"\"\"\n",
"\n",
"CounterButton()"
]
},
{
"cell_type": "markdown",
"id": "296416c3-c6b0-44ee-8ca3-64d9f42acc68",
"metadata": {},
"source": [
":::{note}\n",
"`ReactComponent` extends the [`JSComponent`](JSComponent.md) class, which allows you to create custom Panel components using JavaScript.\n",
"\n",
"`ReactComponent` bears similarities to [`AnyWidget`](https://anywidget.dev/) and [`IpyReact`](https://github.com/widgetti/ipyreact), but `ReactComponent` is specifically optimized for use with Panel and React.\n",
"\n",
"If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md).\n",
":::\n",
"\n",
"## API\n",
"\n",
"### ReactComponent Attributes\n",
"\n",
"- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `render` function which returns the HTML element to display. In a development environment such as a notebook or when using `--dev`, the module will automatically reload upon saving changes. You can use [`JSX`](https://react.dev/learn/writing-markup-with-jsx) and [`TypeScript`](https://www.typescriptlang.org/). The `_esm` script is transpiled on the fly using [Sucrase](https://sucrase.io/). The global namespace contains a `React` object that provides access to React hooks.\n",
"- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved.\n",
"- **`_stylesheets`** (List[str | PurePath] | None): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments.\n",
"\n",
":::note\n",
"You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file its specified in.\n",
":::\n",
"\n",
"#### `render` Function\n",
"\n",
"The `_esm` attribute must export the `render` function. It accepts the following parameters:\n",
"\n",
"- **`model`**: Represents the Parameters of the component and provides methods to add (and remove) event listeners using `.on` and `.off`, render child React components using `.get_child`, get a state hook for a parameter value using `.useState` and to `.send_event` back to Python.\n",
"- **`view`**: The Bokeh view.\n",
"- **`el`**: The HTML element that the component will be rendered into.\n",
"\n",
"Any React component returned from the `render` function will be appended to the HTML element (`el`) of the component.\n",
"\n",
"### State Hooks\n",
"\n",
"The recommended approach to build components that depend on parameters in Python is to create [`useState` hooks](https://react.dev/reference/react/useState) by calling `model.useState('')`. The `model.useState` method returns an array with exactly two values:\n",
"\n",
"1. The current state. During the first render, it will match the initialState you have passed.\n",
"2. The set function that lets you update the state to a different value and trigger a re-render.\n",
"\n",
"Using the state value in your React component will automatically re-render the component when it is updated.\n",
"\n",
"### Callbacks\n",
"\n",
"The `model.on` and `model.off` methods allow registering event handlers inside the render function. This includes the ability to listen to parameter changes and register lifecycle hooks.\n",
"\n",
"#### Change Events\n",
"\n",
"The following signatures are valid when listening to change events:\n",
"\n",
"- `.on('', callback)`: Allows registering an event handler for a single parameter.\n",
"- `.on(['', ...], callback)`: Allows adding an event handler for multiple parameters at once.\n",
"- `.on('change:', callback)`: The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap.\n",
"\n",
"The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap.\n",
"\n",
"#### Bidirectional Events\n",
"\n",
"##### JS -> Python\n",
"\n",
"- `.send_event('', DOMEvent)`: Allows sending browser `DOMEvent` to Python and associating it with a name. An event handler can be registered by name with the `.on_event` method or by implementing a `_handle_` method on the class.\n",
"- `.send_msg(data)`: Allows sending arbitrary data to Python. An event handler can be registered with the `.on_msg(callback)` method on the Python component or by implementing a `_handle_msg` method on the class.\n",
"\n",
"##### Python -> JS\n",
"\n",
"- `._send_event(ESMEvent, data=msg)`: Allows sending arbitrary data to the frontend, which can be observed by registering a handler with `.on('msg:custom', callback)`.\n",
"\n",
"#### Lifecycle Hooks\n",
"\n",
"- `.on('after_layout', callback)`: Called whenever the layout around the component is changed.\n",
"- `.on('after_render', callback)`: Called once after the component has been fully rendered.\n",
"- `.on('resize', callback)`: Called after the component has been resized.\n",
"- `.on('remove', callback)`: Called when the component view is being removed from the DOM.\n",
"\n",
"The `lifecycle:` prefix allows disambiguating lifecycle hooks from change events should a parameter name and lifecycle hook overlap.\n",
"\n",
"## Usage\n",
"\n",
"### Styling with CSS\n",
"\n",
"Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3f11eac1-32c0-405a-9eb3-c5dd715a2d89",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"import param\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"\n",
"class CounterButton(ReactComponent):\n",
"\n",
" value = param.Integer()\n",
"\n",
" _stylesheets = [\n",
" \"\"\"\n",
" button {\n",
" background: #0072B5;\n",
" color: white;\n",
" border: none;\n",
" padding: 10px;\n",
" border-radius: 4px;\n",
" }\n",
" button:hover {\n",
" background: #4099da;\n",
" }\n",
" \"\"\"\n",
" ]\n",
"\n",
" _esm = \"\"\"\n",
" export function render({ model }) {\n",
" const [value, setValue] = model.useState(\"value\");\n",
" return (\n",
" \n",
" );\n",
" }\n",
" \"\"\"\n",
"\n",
"CounterButton()"
]
},
{
"cell_type": "markdown",
"id": "d01593d5-e955-4d4c-a552-f5805f1f93bf",
"metadata": {},
"source": [
"## Send Events from JavaScript to Python\n",
"\n",
"Events from JavaScript can be sent to Python using the `model.send_event` method. Define a handler in Python to manage these events. A *handler* is a method on the form `_handle_(self, event)`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "58fbec62-51fd-4b8b-babf-d09957c19726",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"import param\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"pn.extension()\n",
"\n",
"class EventExample(ReactComponent):\n",
"\n",
" value = param.Parameter()\n",
"\n",
" _esm = \"\"\"\n",
" export function render({ model }) {\n",
" return (\n",
" \n",
" );\n",
" }\n",
" \"\"\"\n",
"\n",
" def _handle_click(self, event):\n",
" self.value = event.data\n",
"\n",
"button = EventExample()\n",
"event_json = pn.pane.JSON(button.param.value)\n",
"\n",
"pn.Column(button, event_json)"
]
},
{
"cell_type": "markdown",
"id": "39dfcddb-09ab-4f87-816a-7a2c83b86ff4",
"metadata": {},
"source": [
"You can also define and send arbitrary data using the `.send_msg()` API and by implementing a `_handle_msg` method on the component:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "458d0dfe-a4e3-4326-999b-856426f1d4d4",
"metadata": {},
"outputs": [],
"source": [
"import datetime\n",
"\n",
"import panel as pn\n",
"import param\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"\n",
"class CustomEventExample(ReactComponent):\n",
"\n",
" value = param.String()\n",
"\n",
" _esm = \"\"\"\n",
" function send_event(model) {\n",
" const currentDate = new Date();\n",
" model.send_msg(currentDate.getTime())\n",
" }\n",
"\n",
" export function render({ model }) {\n",
" return (\n",
" \n",
" );\n",
" }\n",
" \"\"\"\n",
"\n",
" def _handle_msg(self, msg):\n",
" unix_timestamp = msg/1000\n",
" python_datetime = datetime.datetime.fromtimestamp(unix_timestamp)\n",
" self.value = str(python_datetime)\n",
"\n",
"button = CustomEventExample()\n",
"\n",
"pn.Column(button, button.param.value)"
]
},
{
"cell_type": "markdown",
"id": "8dd62b24-2a26-49ff-8aad-386178128167",
"metadata": {},
"source": [
"## Dependency Imports\n",
"\n",
"JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9df3c3ec-fe28-4043-b888-60418eae7e24",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"\n",
"class ConfettiButton(ReactComponent):\n",
"\n",
" _esm = \"\"\"\n",
" import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n",
"\n",
" export function render() {\n",
" return (\n",
" \n",
" );\n",
" }\n",
" \"\"\"\n",
"\n",
"ConfettiButton()"
]
},
{
"cell_type": "markdown",
"id": "0a9fe73e-c552-4cef-85e0-005386520d91",
"metadata": {},
"source": [
"Use the `_importmap` attribute for more concise module references."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5cac202e-f347-4132-b466-bd70713f027c",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"\n",
"class ConfettiButton(ReactComponent):\n",
" _importmap = {\n",
" \"imports\": {\n",
" \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n",
" }\n",
" }\n",
"\n",
" _esm = \"\"\"\n",
" import confetti from \"canvas-confetti\";\n",
"\n",
" export function render() {\n",
" return (\n",
" \n",
" );\n",
" }\n",
" \"\"\"\n",
"\n",
"ConfettiButton()"
]
},
{
"cell_type": "markdown",
"id": "6aec4fdd-152b-4fe8-a545-fd4b9daf989a",
"metadata": {},
"source": [
"See [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more info about the import map format.\n"
]
},
{
"cell_type": "markdown",
"id": "adc92e37-2f42-4166-80ca-712753021cea",
"metadata": {},
"source": [
"## External Files\n",
"\n",
"You can load JSX and CSS from files by providing the paths to these files.\n",
"\n",
"Create the file **counter_button.py**.\n",
"\n",
"```python\n",
"from pathlib import Path\n",
"\n",
"import param\n",
"import panel as pn\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"pn.extension()\n",
"\n",
"class CounterButton(ReactComponent):\n",
"\n",
" value = param.Integer()\n",
"\n",
" _esm = \"counter_button.jsx\"\n",
" _stylesheets = [Path(\"counter_button.css\")]\n",
"\n",
"CounterButton().servable()\n",
"```\n",
"\n",
"Now create the file **counter_button.jsx**.\n",
"\n",
"```javascript\n",
"export function render({ model }) {\n",
" const [value, setValue] = model.useState(\"value\");\n",
" return (\n",
" \n",
" );\n",
"}\n",
"```\n",
"\n",
"Now create the file **counter_button.css**.\n",
"\n",
"```css\n",
"button {\n",
" background: #0072B5;\n",
" color: white;\n",
" border: none;\n",
" padding: 10px;\n",
" border-radius: 4px;\n",
"}\n",
"button:hover {\n",
" background: #4099da;\n",
"}\n",
"```\n",
"\n",
"Serve the app with `panel serve counter_button.py --dev`.\n",
"\n",
"You can now edit the JSX or CSS file, and the changes will be automatically reloaded.\n",
"\n",
"- Try changing `count is {value}` to `COUNT IS {value}` and observe the update.\n",
"- Try changing the background color from `#0072B5` to `#008080`.\n",
"\n",
"## Displaying A Single Child\n",
"\n",
"You can display Panel components (`Viewable`s) by defining a `Child` parameter.\n",
"\n",
"Lets start with the simplest example\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "19868da7-2d89-472c-a5a0-a286bb8319e3",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.custom import Child, ReactComponent\n",
"\n",
"class Example(ReactComponent):\n",
"\n",
" child = Child()\n",
"\n",
" _esm = \"\"\"\n",
" export function render({ model }) {\n",
" return \n",
" }\n",
" \"\"\"\n",
"\n",
"Example(child=pn.panel(\"A **Markdown** pane!\"))"
]
},
{
"cell_type": "markdown",
"id": "9373c469-befd-45d0-8049-3e4dd8be58bb",
"metadata": {},
"source": [
"If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b1b98091-0e75-43c5-bb36-055d36e7bf5d",
"metadata": {},
"outputs": [],
"source": [
"Example(child=\"A **Markdown** pane!\")"
]
},
{
"cell_type": "markdown",
"id": "9f3f2e65-ded4-4e19-8451-d10118a43d41",
"metadata": {},
"source": [
"If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f3419e6f-6990-412c-ab82-fd4fceba98c6",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.custom import Child, ReactComponent\n",
"\n",
"class Example(ReactComponent):\n",
"\n",
" child = Child(class_=pn.pane.Markdown)\n",
"\n",
" _esm = \"\"\"\n",
" export function render({ model }) {\n",
" return \n",
" }\n",
" \"\"\"\n",
"\n",
"Example(child=pn.panel(\"A **Markdown** pane!\"))"
]
},
{
"cell_type": "markdown",
"id": "868da08a-c73f-4338-96c7-4d2f6196fd2a",
"metadata": {},
"source": [
"The `class_` argument also supports a tuple of types:\n",
"\n",
"```python\n",
" child = Child(class_=(pn.pane.Markdown, pn.pane.HTML))\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "4d7d9fd8-a785-459f-95d2-c658547afe84",
"metadata": {},
"source": [
"## Displaying a List of Children\n",
"\n",
"You can also display a `List` of `Viewable` objects using the `Children` parameter type:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cded9c3f-9ce7-4e91-9ff9-cbb84f5074f7",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.custom import Children, ReactComponent\n",
"\n",
"class Example(ReactComponent):\n",
"\n",
" objects = Children()\n",
"\n",
" _esm = \"\"\"\n",
" export function render({ model }) {\n",
" return
{model.get_child(\"objects\")}
\n",
" }\"\"\"\n",
"\n",
"\n",
"Example(\n",
" objects=[\n",
" pn.panel(\"A **Markdown** pane!\"),\n",
" pn.widgets.Button(name=\"Click me!\"),\n",
" {\"text\": \"I'm shown as a JSON Pane\"}\n",
" ]\n",
")"
]
},
{
"cell_type": "markdown",
"id": "bf8087ea-c135-4edb-bb3a-e864f7b7be3e",
"metadata": {},
"source": [
":::note\n",
"You can change the `item_type` to a specific subtype of `Viewable` or a tuple of `Viewable` subtypes.\n",
":::\n",
"\n",
"## Using React Hooks\n",
"\n",
"The global namespace also contains a `React` object that provides access to React hooks. Here is an example of a simple counter button using the `useState` hook:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "472746d9-b237-4f85-9ba1-5abf9b6b130d",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.custom import ReactComponent\n",
"\n",
"class CounterButton(ReactComponent):\n",
"\n",
" _esm = \"\"\"\n",
" let { useState } = React;\n",
"\n",
" export function render() {\n",
" const [value, setValue] = useState(0);\n",
" return (\n",
" \n",
" );\n",
" }\n",
" \"\"\"\n",
"\n",
"CounterButton()"
]
},
{
"cell_type": "markdown",
"id": "6c93522e-ba75-49c9-a8e0-9dffd1c042f6",
"metadata": {},
"source": [
"## References\n",
"\n",
"### Tutorials\n",
"\n",
"- [Build Custom Components](../../how_to/custom_components/esm/custom_layout.md)\n",
"\n",
"### How-To Guides\n",
"\n",
"- [Convert `AnyWidget` widgets](../../how_to/migrate/anywidget/index.md)\n",
"\n",
"### Reference Guides\n",
"\n",
"- [`AnyWidgetComponent`](AnyWidgetComponent.ipynb)\n",
"- [`JSComponent`](JSComponent.ipynb)\n",
"- [`ReactComponent`](ReactComponent.ipynb)"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}