Skip to content

Commit

Permalink
feat(react): add error boundary to fragments (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
tchak authored May 8, 2024
2 parents 1cf9685 + d6e617c commit bafe01d
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/witty-planets-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coldwired/react": patch
---

add error boundary support
24 changes: 7 additions & 17 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
"name": "@coldwired/react",
"description": "React support for @coldwired",
"license": "MIT",
"files": [
"dist"
],
"files": ["dist"],
"main": "./dist/index.cjs.js",
"module": "./dist/index.es.js",
"types": "./dist/types/index.d.ts",
Expand Down Expand Up @@ -34,12 +32,13 @@
"@coldwired/utils": "^0.13.0"
},
"devDependencies": {
"react-error-boundary": "^4.0.13",
"@coldwired/actions": "*",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"html-entities": "^2.4.0",
"zod": "^3.23.4",
"react-aria-components": "^1.2.0"
"react-aria-components": "^1.2.0",
"zod": "^3.23.4"
},
"peerDependencies": {
"react": "^18.0.0",
Expand All @@ -56,24 +55,15 @@
"eslintConfig": {
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-redeclare": "off"
},
"overrides": [
{
"files": [
"vite.config.js",
"vitest.config.ts"
],
"files": ["vite.config.js", "vitest.config.ts"],
"env": {
"node": true
}
Expand Down
32 changes: 31 additions & 1 deletion packages/react/src/root.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@ const Counter = () => {
</div>
);
};
const manifest: Manifest = { Counter, ComboBox, ListBox, ListBoxItem, Popover, Label, Input };
const ComponentWithError = () => {
throw new Error('Boom!');
};
const manifest: Manifest = {
Counter,
ComponentWithError,
ComboBox,
ListBox,
ListBoxItem,
Popover,
Label,
Input,
};

describe('@coldwired/react', () => {
describe('root', () => {
Expand Down Expand Up @@ -56,6 +68,24 @@ describe('@coldwired/react', () => {
root.destroy();
});

it('render with error boundary', async () => {
document.body.innerHTML = `<${DEFAULT_TAG_NAME}>
<${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="Counter"></${REACT_COMPONENT_TAG}>
</${DEFAULT_TAG_NAME}> some text <${DEFAULT_TAG_NAME}>
<${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="ComponentWithError"></${REACT_COMPONENT_TAG}>
</${DEFAULT_TAG_NAME}><div id="root"></div>`;
const root = createRoot(document.getElementById('root')!, {
loader: (name) => Promise.resolve(manifest[name]),
});
await root.mount();
await root.render(document.body).done;

expect(document.body.innerHTML).toEqual(
`<${DEFAULT_TAG_NAME}><div><p>Count: 0</p><button>Increment</button></div></${DEFAULT_TAG_NAME}> some text <${DEFAULT_TAG_NAME}><div role="alert"><pre style="color: red;">Boom!</pre></div></${DEFAULT_TAG_NAME}><div id="root"></div>`,
);
root.destroy();
});

it('render fragment with react aria component', async () => {
document.body.innerHTML = `<${DEFAULT_TAG_NAME}>
<${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="ComboBox">
Expand Down
30 changes: 28 additions & 2 deletions packages/react/src/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type ReactNode,
type ComponentType,
} from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { parseHTMLFragment, isElement } from '@coldwired/utils';

import {
Expand Down Expand Up @@ -43,6 +44,7 @@ export interface RootOptions {
Layout?: ComponentType<{ children: ReactNode }>;
manifest?: Manifest;
schema?: Partial<Schema>;
fallbackRender?: FallbackRender;
}

export interface Schema extends TreeBuilderSchema {
Expand All @@ -64,6 +66,7 @@ export function createRoot(container: Element, options: RootOptions): Root {
const subscriptions = new Set<() => void>();
const manifest: Manifest = Object.assign({}, preloadedManifest);
const schema = Object.assign({}, defaultSchema, options.schema);
const fallbackRender = options.fallbackRender ?? defaultFallbackRender;
const Layout = options.Layout ?? StrictMode;

const notify = () => {
Expand Down Expand Up @@ -148,7 +151,7 @@ export function createRoot(container: Element, options: RootOptions): Root {
createElement(
Layout,
null,
createElement(RootProvider, { subscribe, getSnapshot, onMounted }),
createElement(RootProvider, { subscribe, getSnapshot, onMounted, fallbackRender }),
),
);

Expand Down Expand Up @@ -213,14 +216,27 @@ export function createRoot(container: Element, options: RootOptions): Root {
};
}

export type FallbackRender = (props: FallbackProps & { element: Element }) => ReactNode;

const defaultFallbackRender: FallbackRender = ({ error, element }) => {
const message = element.getAttribute('fallback-message') ?? error.message;
return createElement(
'div',
{ role: 'alert' },
createElement('pre', { style: { color: 'red' } }, message),
);
};

function RootProvider({
subscribe,
getSnapshot,
onMounted,
fallbackRender,
}: {
subscribe(callback: () => void): () => void;
getSnapshot(): Map<Element, ReactNode>;
onMounted: () => void;
fallbackRender: FallbackRender;
}) {
useEffect(onMounted, []);
const cache = useSyncExternalStore(subscribe, getSnapshot);
Expand All @@ -229,7 +245,17 @@ function RootProvider({
Fragment,
null,
...Array.from(cache).map(([element, content]) =>
createPortal(content, element, getKeyForElement(element)),
createPortal(
createElement(
ErrorBoundary,
{
fallbackRender: (props) => fallbackRender({ element, ...props }),
},
content,
),
element,
getKeyForElement(element),
),
),
);
}
Expand Down
30 changes: 21 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit bafe01d

Please sign in to comment.