Skip to content

Commit

Permalink
ReactComponent JS Phoenix LiveView hook to bootstrap React componen…
Browse files Browse the repository at this point in the history
…ts (#2850)
  • Loading branch information
zeorin committed Mar 5, 2025
1 parent 2f6d93c commit 14821de
Show file tree
Hide file tree
Showing 25 changed files with 1,336 additions and 11 deletions.
2 changes: 2 additions & 0 deletions assets/js/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export {
CloseNodePanelViaEscape,
};

export { ReactComponent } from '#/react/hooks';

export const TabIndent = {
mounted() {
this.el.addEventListener('keydown', e => {
Expand Down
33 changes: 33 additions & 0 deletions assets/js/react/components/Boundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { Suspense } from 'react';

import { ErrorBoundary } from 'react-error-boundary';

import { Fallback } from './Fallback';

export type BoundaryProps = {
children?: React.ReactNode;
};

/**
* Some best practices:
*
* - Catch rendering errors and recover gracefully with an [error boundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
* TODO: Configure [`ErrorBoundary`](https://github.com/bvaughn/react-error-boundary#readme)
* - Use a [suspense boundary](https://react.dev/reference/react/Suspense) to allow suspense features (such as `React.lazy`) to work.
* TODO: show a loading ("fallback") UI?
*/
export const Boundary = React.forwardRef<HTMLDivElement, BoundaryProps>(
function Boundary({ children }, ref) {
return (
<ErrorBoundary FallbackComponent={Fallback}>
<Suspense>
<div ref={ref} style={{ display: 'contents' }}>
{children}
</div>
</Suspense>
</ErrorBoundary>
);
}
);
// ESBuild renames the inner component to `Boundary2`
Boundary.displayName = 'Boundary';
12 changes: 12 additions & 0 deletions assets/js/react/components/Fallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { FallbackProps } from 'react-error-boundary';

export type { FallbackProps };

export const Fallback = ({ error }: FallbackProps) => (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: 'red' }}>
{error instanceof Error ? error.message : String(error)}
</pre>
</div>
);
55 changes: 55 additions & 0 deletions assets/js/react/components/Slot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { View } from 'phoenix_live_view';

import { useLayoutEffect, useRef, useState } from 'react';

import { performPatch } from '#/react/lib';

const style: React.CSSProperties = {
// Don't create a box for this element
// https://developer.mozilla.org/en-US/docs/Web/CSS/display#contents
display: 'contents',
};

export type SlotProps = {
view: View;
name: string;
html: string;
cID?: number | null;
};

export const Slot = ({ view, name, html, cID = null }: SlotProps) => {
const [el, setEl] = useState<HTMLDivElement | null>(null);
const unpatchedRef = useRef(true);

// Intentionally referentially stable value, should never change so that React
// never re-renders the child. We want Phoenix to do it for us instead.
const [innerHTML] = useState(() => ({ __html: html }));

useLayoutEffect(() => {
if (
el == null ||
!el.isConnected ||
(innerHTML.__html === html && unpatchedRef.current)
) {
return;
}

performPatch(
view,
el,
`<div data-react-slot=${name} style="display: contents">${html}</div>`,
cID
);

unpatchedRef.current = false;
}, [name, el, innerHTML.__html, view, html, cID]);

return useState(() => (
<div
ref={setEl}
data-react-slot={name}
dangerouslySetInnerHTML={innerHTML}
style={style}
/>
))[0];
};
3 changes: 3 additions & 0 deletions assets/js/react/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Fallback, type FallbackProps } from './Fallback';
export { Boundary, type BoundaryProps } from './Boundary';
export { Slot, type SlotProps } from './Slot';
1 change: 1 addition & 0 deletions assets/js/react/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ReactComponent } from './react-component';
Loading

0 comments on commit 14821de

Please sign in to comment.