Skip to content

Instantly share code, notes, and snippets.

@ManUtopiK
Forked from crutchcorn/VueRenderer.tsx
Created September 9, 2025 11:17
Show Gist options
  • Select an option

  • Save ManUtopiK/05027f7186fc174bb420484174d56a59 to your computer and use it in GitHub Desktop.

Select an option

Save ManUtopiK/05027f7186fc174bb420484174d56a59 to your computer and use it in GitHub Desktop.
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Component } from "vue";
import {
memo,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
useCallback,
} from "react";
import { useGetContexts } from "./react-contexts/use-get-contexts";
import Vue, { provide, ref } from "vue";
/**
* TLDR React doesn't handle errors thrown async
* So this makes async errors thrown in Vue sync React errors so we can handle them better
*
* @see https://github.com/facebook/react/issues/11409#issuecomment-783309449
*/
export function useCrash() {
const [, setState] = useState();
return useCallback(
(err) =>
setState(() => {
throw err;
}),
[],
);
}
interface VueReactRendererProps extends Record<string, any> {
component: Component;
}
function VueRendererBase({ component, ...props }: VueReactRendererProps) {
const crash = useCrash();
const propsRef = useRef(props);
const contexts = useGetContexts();
// eslint-disable-next-line react-hooks/exhaustive-deps
const contextsRef = useMemo(() => ref(contexts), []);
const v = useMemo(() => {
return new Vue({
setup() {
provide("contexts", contextsRef);
},
render: function (h) {
const eventBinds = Object.entries(propsRef.current).reduce(
(prev, [propName, propValue]) => {
const isEvent = /on[A-Z]/.exec(propName);
if (!isEvent) return prev;
// Remove `on` and lowercase only first letter
const uppercaseEventName = propName.slice(2);
/**
* We need to bind both `onEvent` and `on-event` to the same function,
* as React doesn't easily handle binding `-` to JSX props AFAIK
*/
// someEvent
const simpleEventName =
uppercaseEventName.charAt(0).toLowerCase() +
uppercaseEventName.slice(1);
// some-event
const dashCaseEventName = simpleEventName.replace(
/[A-Z]/g,
(letter) => {
return `-${letter.toLowerCase()}`;
},
);
prev[simpleEventName] = propValue;
prev[dashCaseEventName] = propValue;
return prev;
},
{} as Record<string, Function>,
);
return h(component, {
props: propsRef.current,
on: eventBinds,
});
},
errorCaptured: (err) => {
// Ignore network request failures
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).response) return;
crash(err);
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
contextsRef.value = contexts;
}, [contexts]);
const containerRef = useRef<HTMLDivElement>(null);
const vueContainerRef = useRef<HTMLDivElement | null>(null);
const [vm, setVm] = useState(null as any);
useEffect(() => {
if (!vm) return;
propsRef.current = props;
vm.$forceUpdate();
}, [vm, props]);
useLayoutEffect(() => {
return () => {
if (!vm) return;
vm.$destroy();
if (
vueContainerRef.current &&
containerRef.current &&
containerRef.current.contains(vueContainerRef.current)
) {
containerRef.current.removeChild(vueContainerRef.current);
}
vueContainerRef.current = null;
};
}, [vm]);
return (
<div
style={{ display: "contents" }}
ref={(el) => {
containerRef.current = el;
if (!el) return;
if (vm) return;
// Create a separate container for Vue to manage
const vueContainer = document.createElement("div");
vueContainer.style.display = "contents";
el.appendChild(vueContainer);
vueContainerRef.current = vueContainer;
setVm(v.$mount(vueContainer));
}}
/>
);
}
export const VueRenderer = memo(VueRendererBase);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment