manzt / anywidget

jupyter widgets made easy
https://anywidget.dev
MIT License
460 stars 37 forks source link

Minimal Vite React example #185

Closed kolibril13 closed 1 year ago

kolibril13 commented 1 year ago

I've just made an experiment to see if I can run a minimal react app with Anywidget+Vite

export default function App() {
  return <h1>Hello Anywidget + Vite!</h1>;
}

but I get this error: ReferenceError: process is not defined 👇👇👇

image

Steps to reproduce

I've followed the official vite tutorial and the tutorial from the anywidget docs in order to set up a sample project at https://github.com/Octoframes/anywidget-react-vite-test.

The setup process ran without any errors, only displaying the widget does not work. Here are the steps that I did:

  1. npm create vite@latest
    • Project name: .
    • Select a framework: React
    • Select a variant: JavaScript (note that there's also JavaScript+SWC, but I did not choose that)
  2. npm install
  3. npm run dev
  4. Minify the vite default template till the react app is only <h1>Hello Anywidget + Vite!</h1>
  5. Add the following files:
    anywidget-react-vite-test/
    ├── pyproject.toml
    ├── hello_widget/
    │  └── __init__.py
    └── hello.ipynb
  6. Change vite.config.js to
    
    // vite.config.js
    import { defineConfig } from "vite";
    import react from '@vitejs/plugin-react'

export default defineConfig({ plugins: [react()], build: { outDir: "hello_widget/static", lib: { entry: ["src/main.jsx"], formats: ["es"], }, }, });

7. `npm run build` , which adds

```diff
anywidget-react-vite-test/
├── pyproject.toml
├── hello_widget/
│  └── __init__.py
│  └── static/
+       └── main.js
└── hello.ipynb
  1. Setup python
    • python3.11 -m venv .venv && source .venv/bin/activate
    • pip install "anywidget[dev]"
    • pip install jupyterlab
  2. Run Jupyter Lab and execute the cell. Now the process is not defined error will show.

If you currently have some bandwidth, I'd be curious to hear your thoughts on this @manzt @maartenbreddels ✨

manzt commented 1 year ago

Add this to your vite config:

    define: {
      'process.env.NODE_ENV': '"production"'
    },

The issue is that process is a node global and is not in the the browser. For whatever reason, the final main.js includes references to process.env.NODE_ENV which is not defined. This bit of code will effectively have vite transform the the environment check blocks in main.js from:

if (process.env.NODE_ENV !== "production") {

}

to

if ("production" !== "production") {

}

which bundlers can statically analyze and dead-code eliminate.

kolibril13 commented 1 year ago

Thanks for the quick reply! I've now added 'process.env.NODE_ENV': '"production"' and run npm run build again, now I get this new error message in the notebook:

image
[Open Browser Console for more detailed log - Double click to close this message]
Failed to create view for 'AnyView' from module 'anywidget' with model 'AnyModel' from module 'anywidget'
Error: Minified React error #299; visit https://reactjs.org/docs/error-decoder.html?invariant=299 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at ye.createRoot (blob:http://localhost:8888/8a12665c-753f-4e50-9bda-b17ff0c591c0:6140:11)
    at blob:http://localhost:8888/8a12665c-753f-4e50-9bda-b17ff0c591c0:6211:4
kolibril13 commented 1 year ago

and one further observation: running npm run dev with define: {'process.env.NODE_ENV': '"production"'} in vite.config.js does not show the content of the component in the browser anymore. I can only see a blank page with this error in the console:

App.jsx:8 Uncaught Error: @vitejs/plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201
    at App.jsx:8:11
(anonymous) @   App.jsx:8
manzt commented 1 year ago

hmm, this could be done to the new automatic runtime in defaults in @vitejs/plugin-react. Can you try react({ jsxRuntime: 'classic' })?

kolibril13 commented 1 year ago

Thanks for the suggestion, nope, that did not work. Before:

image

After:

image
manzt commented 1 year ago

with the classic runtime you must import * as React from "react"; at the top of any jsx file.

manzt commented 1 year ago

Ok, I got it sorted out. The issue is that you are bundling react in the library mode in vite. You need the process.env.NODE_ENV to be define during the build but not during development:

// vite.config.js
import { defineConfig } from "vite";
import react from '@vitejs/plugin-react'

export default defineConfig(({ command }) => {
    let define = {};
    if (command === "build") {
        define["process.env.NODE_ENV"] = JSON.stringify("production");
    }
    return {
        plugins: [react()],
        build: {
            outDir: "hello_widget/static",
            lib: {
                entry: ["src/main.jsx"],
                formats: ["es"],
            },
        },
        define,
    }
});
kolibril13 commented 1 year ago

Nice, now I have App.jsx:

import * as React from "react";

export default function App() {
  return <h1>Hello Anywidget + Vite!</h1>;
}

and

// vite.config.js
import { defineConfig } from "vite";
import react from '@vitejs/plugin-react'

export default defineConfig(async ({ command }) => {
    let define = {};
    if (command === "build") {
        define["process.env.NODE_ENV"] = JSON.stringify("production");
    }
    return {
        plugins: [react()],
        build: {
            outDir: "hello_widget/static",
            lib: {
                entry: ["src/main.jsx"],
                formats: ["es"],
            },
        },
        define,
    }
});

this way the website works now again.

What remains is the

[Open Browser Console for more detailed log - Double click to close this message]
Failed to create view for 'AnyView' from module 'anywidget' with model 'AnyModel' from module 'anywidget'
Error: Minified React error #299; visit https://reactjs.org/docs/error-decoder.html?invariant=299 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at ye.createRoot (blob:http://localhost:8888/8afd7d22-37e7-4ebf-82c9-124781fe5554:6140:11)
    at blob:http://localhost:8888/8afd7d22-37e7-4ebf-82c9-124781fe5554:6211:4

error in the widget:

image
manzt commented 1 year ago

This makes sense. Your main.jsx isn't a valid anywidget module. (It doesn't export a render function).

If you want to make a react app with Vite, I'd recommend following the docs and using the anywidget plugin. The @vitejs/plugin-react react plugin doesn't play well inside Jupyter and does not offer the same HMR experience as our plugin.

manzt commented 1 year ago

If you want to develop separately, I'd recommend creating a widget.jsx that acts as the ESM entrypoint for your widget

// widget.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

export function render({ model, el }) {
  let root = ReactDOM.createRoot(el);
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
  );
  return () => root.unmount();
}

and update your build with:


// vite.config.js
import { defineConfig } from "vite";
import react from '@vitejs/plugin-react'

export default defineConfig(async ({ command }) => {
    let define = {};
    if (command === "build") {
        define["process.env.NODE_ENV"] = JSON.stringify("production");
    }
    return {
        plugins: [react()],
        build: {
            outDir: "hello_widget/static",
            lib: {
++              entry: ["src/widget.jsx"],
                formats: ["es"],
            },
        },
        define,
    }
});
kolibril13 commented 1 year ago

This makes sense. Your main.jsx doesn't export a render function.

If you want to make a react app with Vite, I'd recommend following the docs and using the anywidget plugin. The @vitejs/plugin-react react plugin doesn't play well inside Jupyter and does not offer the same HMR experience as our plugin.

Nice, I will try that out and will let you know soon if I was able to get it working! :)

kolibril13 commented 1 year ago
image

ok, I've now added and installed the vite plugin from https://anywidget.dev/en/bundling/#development-1 and tried to run npm run build, but that lead to the Minified React error again.

image

Getting this minimal example working with vite would be really awesome! May I ask you if you could set that up? In return, I can offer some cool anywidget apps that might come from that. I just sent you an invitation to the repo, feel free to add commits directly to main. But also if you don't have time for that, I really appreciate your help so far ✨

kolibril13 commented 1 year ago

ohhh. I just tried to widget.jsx approach from two messages above https://github.com/manzt/anywidget/issues/185#issuecomment-1638375731 at that works!!! 🎉 Both in browser and in the widget! 🌟

kolibril13 commented 1 year ago

and packages work as well, eg. react-qr-code! 🤩

image
kolibril13 commented 1 year ago

and interactive mafs components render as well! 🎉

https://github.com/manzt/anywidget/assets/44469195/f0b4e2d3-07d6-4268-831b-02e2108e7de5

maartenbreddels commented 1 year ago

If you currently have some bandwidth, I'd be curious to hear your thoughts on this @manzt @maartenbreddels ✨

This is how I see the next step up. First, play around with ipyreact, nothing needed except a browser. If you want to package this, or deploy this in production, you probably want to build a bundle using a modern toolchain ala vite or esbuild. I haven't tried this myself yet, so thanks for showing the pitfalls :)

kolibril13 commented 1 year ago

so thanks for showing the pitfalls :)

It's a pleasure to be on this discovery journey with the two of you! ⛵

The last step, shipping to pypi via poetry, was almost TOO easy 🏝️🤩 I just published ipymafs to pypi, feel free to try is via pip install ipymafs! I think I will record a video tutorial at some point about the whole process, so that it becomes easier for other people to reproduce this whole widget creation workflow and can avoid all the deadlocks that I took. :)

I still have some remaining questions. @manzt : Would you be interested in helping me polish ipymafs? That project could then also become a anywidget vite react example project.

For these questions, I would then create separate issues.

kolibril13 commented 1 year ago

Just stumbled about the first question: How do I pass props using the anywidget+react+vite approach? I'm using the react-qr-code widget as a minimal example in this repo. My idea was to do it the exact same way like in ipyreact https://kolibril13.github.io/ipyreact-example-gallery/01-nb.html#qr-code-widget

but when I change

--       return <QRCode value="Hiiii" />
++       return <QRCode value={content} />

then the output is just white, without the rendered content.

image
kolibril13 commented 1 year ago

ah, it seems like widget.jsx needs to pass the prop as well. With that, return <QRCode value={content} /> works now. However, how can I sync <App content="And here is some content!!!!!!!!" /> now with the value of my traitlet? In the below example, I want the traitlet to give the content "Hi" to the qr code, but that's currently not the case.

image
manzt commented 1 year ago

How do I pass props using the anywidget+react+vite approach?

You need to connect your "app" to the Jupyter Widget model. I've tried to detail the important bits of connecting JS with Python in the docs.

But, most simply:

// widget.jsx
+      <App content={model.get("content")} />

This code grabs the initial value of content and feeds it as a prop to your component.

To perhaps anticipate your next question, "How do you update or re-render the component when content changes?" ipyreact takes care of setting up this data-binding for you, but anywidget requires some more setup because it's not React-specific and forwards the primitives from the Jupyter Widgets framework.

From the anywidget docs:

[Y]our widget’s render function is executed exactly one per output cell that displays the widget instance. Therefore, render primarily serves two purposes:

  1. Initializing content to display (i.e., create and append element(s) to context.el)
  2. Registering event handlers to update or display model state any time it changes (i.e., passing callbacks to model.on)

The code in widget.jsx only accomplishes 1 at the moment. Jupyter Widgets is based on a Model-View-Controller (MVC) architecture using backbone JS. React is not an MVC framework React is not an MVC framework.

To accomplish 2, you'll need to wire up some React hooks around the model in your render function.

// widget.jsx

// Connects React App component to the Backbone model
function WidgetAdapter({ model }) {
  let [content, setContent] = React.useState(model.get("content"));
  React.useEffect(() => {
    // update content anytime the model changes (from either Python or JS)
    model.on("change:content", () => setContent(model.get("content")));
  }, [])
  return <App content={content} />
}

export function render({ model, el }) {
  let root = ReactDOM.createRoot(el);
  root.render(
    <React.StrictMode>
      <WidgetAdapter model={model} />
    </React.StrictMode>,
  );
  return () => root.unmount();
}
manzt commented 1 year ago

I think I will record a video tutorial at some point about the whole process, so that it becomes easier for other people to reproduce this whole widget creation workflow and can avoid all the deadlocks that I took. :)

This would be great! I've also been meaning to record some videos. Maybe we could chat sometime about how to improve the anywidget docs / create more resources.

@manzt : Would you be interested in helping me polish ipymafs?

Sure, I'll have a look.

kolibril13 commented 1 year ago

<App content={model.get("content")} />

awesome, that works! 🎉 😊

To perhaps anticipate your next question, "How do you update or re-render the component when content changes?"

when I first saw anywidget I was already suspicious that you're a magician, but now it's confirmed! 🧙

just incorporated that hook for updating from the traitlet as well, and also works out of the box 🎉

This would be great! I've also been meaning to record some videos. Maybe we could chat sometime about how to improve the anywidget docs / create more resources.

I'm absolutely up for that, looking forward to connecting with you! My favorite communication platform is discord, my username there is the same as here: kolibril13 Otherwise, DM on Twitter would also work.

Sure, I'll have a look.

Amazing, thank you so much for your time and support, I really appreciate that. I was thinking instead of polishing ipymafs directly, it would be more convenient to have the most simple widget possible, which is I think the react-qr-code widget with only 4 lines of code. As soon as that minimal example is polished, that project can serve as a best practice template on how to set up a react-vite-anywidget project.

One remaining polishing challenge: Currently, every change in the qr code widget needs npm run build and kernel restart, so it would be it would be a great relief if there was a way to enable HMR for the react-vite-anywidget stack.

This minimal qr-code example currently lives at https://github.com/Octoframes/anywidget-react-vite-test, feel free to add commits directly to main there.

manzt commented 1 year ago

I'm going to close this issue, in favor of #190 and our on-going discussion in anywidget-react-vite-test.

manzt commented 1 year ago

My favorite communication platform is discord, my username there is the same as here: kolibril13 Otherwise, DM on Twitter would also work.

Sounds good. I am at a conference / traveling the next couple of weeks but will reach out to find a time to chat.