Reuse React components from plugin

164 views
Skip to first unread message

Diego de la Hera

unread,
Dec 21, 2020, 2:50:54 PM12/21/20
to zotero-dev
Hi all! I'm writing a bootstrapped plugin for Zotero and I'm trying to stay away as much as possible from XUL, and using React/HTML instead.
I would like the plugin to have the Zotero's look and feel, and I would like to reuse some of the React components already available in Zotero's codebase, such as those used by the Tags Box (e.g., Editable, Input, TextAreaInput).
I have some experience with Javascript, but I still have LOTS to learn. Would there be a way to use these native components from the plugin without having to copy them to the plugin's code?
Thank you!

Dan Stillman

unread,
Dec 22, 2020, 3:03:09 AM12/22/20
to zoter...@googlegroups.com
On 12/21/20 2:50 PM, Diego de la Hera wrote:
> I'm writing a bootstrapped plugin for Zotero and I'm trying to stay
> away as much as possible from XUL, and using React/HTML instead.
> I would like the plugin to have the Zotero's look and feel, and I
> would like to reuse some of the React components already available in
> Zotero's codebase, such as those used by the Tags Box (e.g., Editable,
> Input, TextAreaInput).
> […] Would there be a way to use these native components from the
> plugin without having to copy them to the plugin's code?

You can, but note that Zotero's use of React is still fairly limited,
and we haven't made extensive use of those low-level components, so they
may not do all that much for you. But you can try. Editable specifically
could be useful if you hook it up correctly.

I haven't tested this, but the main thing would be making sure you're
using Zotero's require(), so that you can require the bundled
components. That require() should be available as long as you've
included chrome://zotero/content/include.js in your window.

You'd do something like this (assuming you're using Babel or similar to
transpile ES6 modules to CommonJS — if not, you can forego the ES6
imports/exports and use `require()` and such explicitly):

main.js:

```
import React from 'react';
import ReactDOM from 'react-dom';
import Foo from 'foo.js';

var Plugin = new function () {
    this.onLoad = function () {
        ReactDOM.render(
            <Foo/>,
            document.getElementById('foo')
        );
    }
}


```

foo.js:

```
import React, { useState } from 'react';
import Editable from 'components/editable.js';

function Foo() {
    const [active, setActive] = useState(false);

    function handleClick() {
        setActive(true);
    }

    return (
        <div>
            <Editable
                isActive={active}
                onClick={handleClick}
                value="Foo"
            />
        </div>
    );
}

export default Foo;
```

This would be mostly broken but shows the basic idea. Sorry we don't yet
have a sample extension that takes advantage of this stuff.

- Dan

Diego de la Hera

unread,
Dec 23, 2020, 4:09:27 PM12/23/20
to zotero-dev
Thank you for your prompt reply, Dan! I will try what you suggest and let you know what happened :)

Diego de la Hera

unread,
Dec 24, 2020, 3:33:28 PM12/24/20
to zotero-dev
This seems to have worked correctly!! Thank you

I would like to mention a few details, in case somebody else needs this in the future:

As I'm using webpack, I had to configure these modules as commonjs `externals` in `webpack.config.ts`. This way webpack doesn't try to bundle them, and they are retrieved as external dependencies at runtime.

So, for example, I import the Editable like this:

import Editable from '@zotero/components/editable';

And add this to `webpack.config.ts`:

module.exports = {
  ...,
  externals: {
    '@zotero/components/editable': 'commonjs components/editable'
  },
  ...
}

One thing about using the Button component (components/button). It uses `react-intl`, so it relies on there being a <IntlProvider> in its ancestry. In my code I was importing `IntlProvider` using regular imports (that is, using my plugin's own `react-intl` dependency) and wrapping the `Button` inside this `IntlProvider`. But it was failing with "[React Intl] Could not find required `intl` object. <IntlProvider> needs to exist in the component ancestry". I understand this was because the Button's react-intl context (Zotero) was not the same as my plugin's. I solved this by importing react-intl as an external dependency as well:

// my script
import { IntlProvider } from '@zotero/react-intl';

// webpack.config.ts
module.exports = {
  ...,
  externals: {
    '@zotero/react-intl': 'commonjs react-intl'
  },
  ...
}

Hope this helps someone else, and let me know if you think this may be wrong or may cause problems in any way.

Thanks!

Diego

Diego de la Hera

unread,
Dec 28, 2020, 12:57:06 PM12/28/20
to zotero-dev
Related to this thread, I would like to report what I think is a small bug in Zotero's Input component. As far as I know, this component is only used in the Editable component, which again so far seems to be used only in the TagsBox component. Here, the component is called with the `autoComplete` prop set to `true`, but the Input component seems to be designed to be used with this prop set to `false` as well.
However, if done so, the Input's `handleChange` method fails because it is called without the `options` parameter. The regular HTML input element's (used when autoComplete=false) onChange event doesn't send a second `options` parameter, whereas the Autosuggest custom component (used when autoComplete=true) does.
I guess this could be easily fixed by checking if `options` is defined in `handleChange`. I submitted a pull request here.
This doesn't happen with the related TextArea Component because the `handleChange` function there doesn't expect a second `options` parameter.

The workaround I'm using for now is to use the Input component with autoComplete=true and provide a function that simply returns an empty array as `getSuggestions` prop.

Diego de la Hera

unread,
Jan 6, 2021, 6:47:04 PM1/6/21
to zotero-dev
In case someone finds this useful in the future, I would like to add that I came up with what I think is a better solution for the IntlProvider problem I mentioned above. Instead of importing and using the IntlProvider context from Zotero's react-intl (which by the way caused a warning about multiple renderers concurrently rendering the same context provider), I created a plugin Button component which simply (1) imports Zotero's Button component, (2) unwraps it from its injectIntl HOC, and (3) exports it again, wrapped using the plugin's react-intl injectIntl.

You can see what I mean in the source code of the plugin I'm working on, here and here.

Arjan

unread,
Mar 3, 2023, 11:41:56 AM3/3/23
to zotero-dev
How should the main window be made available when adding a React component to be rendered with ReactDOM.render()? (Or rather, with `root = createRoot(domElement)` and `root.render()`, for v18)

I get "ReferenceError: window is not defined" when trying this.

The plugin is setup to use esbuild in order to allow using NPM packages.
Message has been deleted
Message has been deleted
Message has been deleted

volatile static

unread,
Mar 5, 2023, 9:20:23 AM3/5/23
to zotero-dev
Reply all
Reply to author
Forward
0 new messages