{ "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 }