widgetti / ipyreact

React for ipywidgets that just works. No webpack, no npm, no hassle
BSD 3-Clause "New" or "Revised" License
104 stars 8 forks source link

Call of python functions from frontend when switching form ipyreact to anywidget #43

Closed kolibril13 closed 4 months ago

kolibril13 commented 10 months ago

Hi @manzt, @maartenbreddels, hope you're doing well!

Yesterday I was translating the whiteboard widget jupyter-tldraw from ipyreact to anywidget. I think it's an amazing experience to first make a quick draft in ipyreact, and then move on to build a proper widget using anywidget.

During the translation, I ran into one problem. In ipyreact I can call a function on_my_python_function from the react frontend (introduced in https://github.com/widgetti/ipyreact/pull/8)

But translating this to anywidget does not work out of the box. Instead of calling the python function from react directly, I defined a state in react and observed it from the python side. That's a bit more code, but also feels more natural to do, as frontend and backend have a more clear separation.

If you have capacity and interest in finding a best practice here, I'd be happy to hear your thoughts on this! ✨

In ipyreact, I can call a python function from react like this:

import ipyreact
from traitlets import Int, Unicode

class Widget1(ipyreact.ReactWidget):
    my_count = Int(0).tag(sync=True)
    label = Unicode("Click me").tag(sync=True)

    def on_my_python_function(self):
        self.my_count = self.my_count + 1
        self.label = f"Clicked {self.my_count}"

    _esm = """
        import * as React from "react";

        export default function ({ label, on_my_python_function }) {
          function handleClick() {
            console.log("button clicked");
            on_my_python_function()
          }
          return (
            <div>
              <button onClick={handleClick}>{label}</button>
            </div>
          );
        }
    """

Widget1()

that works totally fine.

Now, I want to do the same in anywidget:

import anywidget
from traitlets import Int, Unicode
import pathlib

class Widget2(anywidget.AnyWidget):
    my_count = Int(0).tag(sync=True)
    label = Unicode("Click me").tag(sync=True)

    def on_my_python_function(self):
        self.my_count = self.my_count + 1
        self.label = f"Clicked {self.my_count}"

    _esm = pathlib.Path.cwd() / "src" / "tldraw" / "static" / "testing.js"

w2 = Widget2()
w2
// this is at "src/myprojectname/static/testing.jsx"
import * as React from "react";
import { createRender, useModelState } from "@anywidget/react";

export const render = createRender(() => {
  const [label] = useModelState("label");

  function handleClick() {
    console.log("button clicked");
    // on_my_python_function()
  }

  return (
    <div>
      <button onClick={handleClick}>{label}</button>
    </div>
  );
});

but there, I can not call on_my_python_function within the component (or is there a way to do so that I'm not aware of?).

And finally, here the way of solving this using state and observe:

import * as React from "react";
import { createRender, useModelState } from "@anywidget/react";

export const render = createRender(() => {
  const [label] = useModelState("label");
  const [clicks, setClicks] = useModelState("clicks");

  function handleClick() {
    console.log("button clicked");
    setClicks(clicks + 1);  // instead of on_my_python_function()
  }

  return (
    <div>
      <button onClick={handleClick}>{label}</button>
    </div>
  );
});
import anywidget
from traitlets import Int, Unicode, observe
import pathlib

class Widget3(anywidget.AnyWidget):
    my_count = Int(0).tag(sync=True)
    label = Unicode("Click me").tag(sync=True)
    clicks = Int(0).tag(sync= True)

    def on_my_python_function(self):
        self.my_count = self.my_count + 1
        self.label = f"Clicked {self.my_count}"

    @observe("clicks")
    def _observe_count(self, change):
        self.on_my_python_function()

    _esm = pathlib.Path.cwd() / "src" / "tldraw" / "static" / "testing.js"

Widget3()

So, what do you think is the best way to develop solid widgets?

Implementing a feature that allows on_my_python_function calls also from anywidget? Or even deprecating this feature in ipyreact and only encouraging the state+observe approach?

manzt commented 10 months ago

So, what do you think is the best way to develop solid widgets?

I think both approaches allow you build "solid widgets". ipyreact removes all the boilerplate to get started with React and Jupyter Widgets, but a tradeoff is that it is slightly more opinionated in ways that probably make sense for that use case but not anywidget as a whole.

anywidget is intended to be a wrapper around standard Jupyter Widgets, which the front-end is based on backbone JS originally. It solves that problem well, and allows frameworks like ipyreact to exist (making the React use case dead simple). I am trying to make @anywidget/* client-side libraries to offer reusable patterns for using different frontend frameworks with anywidget, but I don't anticipate these will ever end up in the core of anywidget.

By avoiding coupling the Python <> JS APIs further, it means the same python class can be used for various front-end frameworks. ESM (core of anywidget's design) is a web standard and therefore very stable target to build around (compared to something like JSX and React, which poses challenges with versions over time).

maartenbreddels commented 4 months ago

Note that ipyreact now has a way to work with bundles (it always could, but there was no example), not having to rely on esm.sh: https://github.com/widgetti/ipyreact?tab=readme-ov-file#bundled-esm-modules

Note that this is indirect, by first providing a bundle, and then importing that from the widget. Similar to anywidget you can also target the bundle for the widget directly (as the expense of uploading large bundles many times).

@kolibril13 not sure if this closes the issue

kolibril13 commented 4 months ago

Good to hear! Yes, I ll close this issue now!