Re-rendering with a custom render function

I'm using a custom render and getRootContainer function in order to mount a custom element directly onto a specific pre-existing div without the use of shadow root (in order to adopt the existing page styles) and to avoid using any typical Plasmo parent elements. However, unlike the default exported component which automatically re-renders when the root is removed and added back in, this custom render does not adopt the functionality. When my custom root container that I've declared is removed from the page and added back in, my custom BetterInput component does not re-render. What is the best way to solve this problem? The following is my code:
export const getRootContainer = () =>
new Promise((resolve) => {
const checkInterval = setInterval(() => {
const rootContainerParent = document.querySelector("._2-F7v")
if (rootContainerParent) {
clearInterval(checkInterval)
resolve(rootContainerParent)
}
}, 10)
});

// Use this to optimize unmount lookups
export const getShadowHostId = () => "better-input";

// Override render to directly place custom component without parent
export const render: PlasmoRender = async ({
// anchor, // the observed anchor, OR document.body.
createRootContainer // This creates the default root container
}) => {
const rootContainer = await createRootContainer()

const root = createRoot(rootContainer) // Any root
root.render(
<BetterInput/>
)
}

const BetterInput = () => {
return (
<input
className="better-input"
placeholder="Type here..."
>
</input>
)
};
export const getRootContainer = () =>
new Promise((resolve) => {
const checkInterval = setInterval(() => {
const rootContainerParent = document.querySelector("._2-F7v")
if (rootContainerParent) {
clearInterval(checkInterval)
resolve(rootContainerParent)
}
}, 10)
});

// Use this to optimize unmount lookups
export const getShadowHostId = () => "better-input";

// Override render to directly place custom component without parent
export const render: PlasmoRender = async ({
// anchor, // the observed anchor, OR document.body.
createRootContainer // This creates the default root container
}) => {
const rootContainer = await createRootContainer()

const root = createRoot(rootContainer) // Any root
root.render(
<BetterInput/>
)
}

const BetterInput = () => {
return (
<input
className="better-input"
placeholder="Type here..."
>
</input>
)
};
7 Replies
Kiran
Kiran•5mo ago
I have found a working solution, with reusable functions that automatically enable any component to be directly injected without a shadow root and without parent elements. It is quite large/messy, so I will update it and post the solution later.
Isitfato
Isitfato•5mo ago
Thanks—curious to see what your solution is. Currently facing the same issue and it’s pretty much the last thing left to release my extension
Arcane
Arcane•5mo ago
@Alex Godard has reached level 2. GG!
Kiran
Kiran•5mo ago
Here's the finished example. I put this in a file called core.tsx in a separate directory outside the contents folder.
Kiran
Kiran•5mo ago
Kiran
Kiran•5mo ago
Example usage of a component from my project:
import type {PlasmoCSConfig, PlasmoRender} from "plasmo"
import {coreGetRootContainer, renderComponent} from "~core/core";

// --------------------------------------------------------------------------------
// Config
export const config: PlasmoCSConfig = {matches: ["https://www.duolingo.com/*"]}
export const getRootContainer = () => coreGetRootContainer("._2-F7v", 'better-input', BetterInput);
export const render: PlasmoRender<any> = async ({createRootContainer}) =>
renderComponent(await createRootContainer() as HTMLElement, BetterInput);

// --------------------------------------------------------------------------------
// Component
const BetterInput = () => {
return (
<input
id="better-input"
placeholder="Type here..."
>
</input>
)
};

export default BetterInput;
import type {PlasmoCSConfig, PlasmoRender} from "plasmo"
import {coreGetRootContainer, renderComponent} from "~core/core";

// --------------------------------------------------------------------------------
// Config
export const config: PlasmoCSConfig = {matches: ["https://www.duolingo.com/*"]}
export const getRootContainer = () => coreGetRootContainer("._2-F7v", 'better-input', BetterInput);
export const render: PlasmoRender<any> = async ({createRootContainer}) =>
renderComponent(await createRootContainer() as HTMLElement, BetterInput);

// --------------------------------------------------------------------------------
// Component
const BetterInput = () => {
return (
<input
id="better-input"
placeholder="Type here..."
>
</input>
)
};

export default BetterInput;
Essentially, it just requires you to to call coreGetRootContainer with the parent selector where you want to inject the component, the id that you gave to the component (it's required to give an id in jsx), and then just pass the actual component. The other method is renderComponent which doesn't require anything custom, just follow the same example to pass a root container element. How it works: It replaces plasmo's renderer with a custom renderer. This was specifically designed to not use shadow root and inject directly instead to inherit the page styles. After the initial render, there is a DOM observer that gets created to continuously watch all DOM changes. When it finds out that the component no longer exists, but the parent element still does, it will re-inject it. (example - you want to inject a button inside a div. The user is on an SPA so the div disappears along with your button when they move to a new page. Then, they move back to that same page, so it detects that the div showed up again but without the button, so your button gets re-injected). Overall pretty hacky but it worked. Also leaving this message for anyone else who had issues with Plasmo - I actually just ended up migrating my extension from plasmo to CRXJS today (https://crxjs.dev/vite-plugin). I was able to write the same solution in much less lines of code. Plasmo is also just way too buggy for me - the extension doesn't even update half the time when I edit a file and I often have to manually kill plasmo and restart the process to get it to update. The content UI HMR is also extremely buggy, and doesn't work at all for global css. CRXJS feels much faster and has perfect HMR. It has less features, but more third party libraries are just working out of the box since it uses Vite, and it gives much more control over the DOM which I enjoyed a lot more.
Isitfato
Isitfato•5mo ago
It works -- Thank you!! Kinda nuts that you had to come up with this and that there's no example in the official docs -- This seems like a very common use case
Want results from more Discord servers?
Add your server