{ "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": "b821b14e", "metadata": {}, "source": [ "## Send Events from Python to JavaScript\n", "\n", "Equivalently, events from Python can be sent to JavaScript using the `ReactComponent._send_msg` method. To define a handler to receive these messages register a callback with `model.on('msg:custom', callback)`:\n", "\n", "In this simple example, we send a message containing the current date and time and display it in the component (note the serializer turns our datetime object into a timestamp):" ] }, { "cell_type": "code", "execution_count": null, "id": "dbaaf046", "metadata": {}, "outputs": [], "source": [ "import datetime\n", "\n", "from panel.custom import ReactComponent\n", "\n", "\n", "class PythonToJSExample(ReactComponent):\n", "\n", " _esm = \"\"\"\n", " export function render({ model }) {\n", " const [value, setValue] = React.useState(null)\n", "\n", " model.on('msg:custom', (msg) => {\n", " setValue(new Date(msg).toLocaleString())\n", " })\n", "\n", " return
{value}
\n", " }\n", " \"\"\"\n", "\n", " def send_event(self):\n", " self._send_msg(datetime.datetime.now())\n", "\n", "py2js = PythonToJSExample()\n", "\n", "py2js" ] }, { "cell_type": "markdown", "id": "1ac07d2e", "metadata": {}, "source": [ "Sending messages as events rather than state changes provides more control over state synchronization between Python and JavaScript." ] }, { "cell_type": "code", "execution_count": null, "id": "28aabb6e", "metadata": {}, "outputs": [], "source": [ "py2js.send_event()" ] }, { "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": "06dc3bd5", "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", "## Rendering into a specific DOM element\n", "\n", "You can render the component into a specific DOM element by providing a `root_node` to the underlying `ReactComponent` model. This allows you to render things outside of the regular DOM hierarchy.\n", "\n", "The `root_node` should be a valid CSS selector for the DOM element you want to render into, if it doesn't exist it will be created and appended to the document body.\n", "\n", "In this example we render the component into a div with the id `custom-root` which we then place in the upper right corner of the document." ] }, { "cell_type": "code", "execution_count": null, "id": "2076e701", "metadata": {}, "outputs": [], "source": [ "import panel as pn\n", "\n", "from panel.custom import ReactComponent\n", "\n", "\n", "class RootNodeExample(ReactComponent):\n", "\n", " _esm = \"\"\"\n", " export function render({ model }) {\n", " return
Hello
\n", " }\n", " \"\"\"\n", "\n", " def _get_properties(self, doc):\n", " props = super()._get_properties(doc)\n", " props[\"root_node\"] = \"#custom-root\"\n", " return props\n", " \n", "RootNodeExample()" ] }, { "cell_type": "markdown", "id": "bf8087ea-c135-4edb-bb3a-e864f7b7be3e", "metadata": {}, "source": [ "\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 }