Skip to content

Commit

Permalink
feat(react): lazy load react (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
tchak authored May 23, 2024
2 parents 78a7172 + 5b8a5d1 commit 4c47a36
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 238 deletions.
5 changes: 5 additions & 0 deletions .changeset/unlucky-roses-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coldwired/react": minor
---

lazy load react
10 changes: 8 additions & 2 deletions packages/react/src/actions-react-plugin.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import { useState } from 'react';
import { Actions } from '@coldwired/actions';
import { encode as htmlEncode } from 'html-entities';

import { createRoot, createReactPlugin, defaultSchema, type Manifest, type Root } from '.';
import type { ReactComponent } from './react-tree-builder';
import {
createRoot,
createReactPlugin,
defaultSchema,
type Manifest,
type Root,
type ReactComponent,
} from '.';

const NAME_ATTRIBUTE = defaultSchema.nameAttribute;
const PROPS_ATTRIBUTE = defaultSchema.propsAttribute;
Expand Down
6 changes: 5 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export * from './root';
export * from './plugin';
export * from './observable';
export { hydrate, preload, createReactTree, createState, type State } from './react-tree-builder';
export { preload } from './preload';

export type { State } from './state.react';
export type { LayoutComponent, ErrorBoundaryFallbackComponent } from './root.react';
export type { ReactElement, ReactComponent, ReactValue } from './tree-builder.react';
1 change: 0 additions & 1 deletion packages/react/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export function createReactPlugin(root: Root): Plugin {
});
pending.add(ready);
const mountAndRender = async () => {
await root.mount();
const batch = root.render(element);
if (batch.count != 0) {
await batch.done;
Expand Down
27 changes: 27 additions & 0 deletions packages/react/src/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { DocumentFragmentLike, Manifest, Schema } from './tree-builder.react';

export const defaultSchema: Schema = {
componentTagName: 'react-component',
slotTagName: 'react-slot',
nameAttribute: 'name',
propsAttribute: 'props',
};

export function preload(
documentOrFragment: Document | DocumentFragmentLike,
loader: (names: string[]) => Promise<Manifest>,
schema?: Partial<Schema>,
): Promise<Manifest> {
const { componentTagName, nameAttribute } = Object.assign({}, defaultSchema, schema);
const components = documentOrFragment.querySelectorAll(componentTagName);
const componentNames = new Set(
Array.from(components).map((component) => {
const name = component.getAttribute(nameAttribute);
if (!name) {
throw new Error(`Missing "${nameAttribute}" attribute on <${componentTagName}>`);
}
return name;
}),
);
return loader([...componentNames]);
}
2 changes: 0 additions & 2 deletions packages/react/src/root-schema.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ describe('@coldwired/react', () => {
componentTagName: REACT_COMPONENT_TAG,
},
});
await root.mount();
await root.render(document.body).done;

expect(document.body.innerHTML).toEqual(
Expand All @@ -54,7 +53,6 @@ describe('@coldwired/react', () => {
componentTagName: REACT_COMPONENT_TAG,
},
});
await root.mount();
await root.render(document.body).done;

expect(document.body.innerHTML).toEqual(
Expand Down
103 changes: 103 additions & 0 deletions packages/react/src/root.react.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { createPortal } from 'react-dom';
import { createRoot as createReactRoot } from 'react-dom/client';
import {
useSyncExternalStore,
useEffect,
StrictMode,
type ReactNode,
type FunctionComponent,
} from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

type LayoutProps = { children: ReactNode };
export type LayoutComponent = FunctionComponent<LayoutProps>;
type ErrorBoundaryFallbackProps = FallbackProps & { element: Element };
export type ErrorBoundaryFallbackComponent = FunctionComponent<ErrorBoundaryFallbackProps>;

export { hydrate } from './tree-builder.react';

export function createAndRenderReactRoot({
container,
subscribe,
getSnapshot,
onMounted,
LayoutComponent,
ErrorBoundaryFallbackComponent,
}: {
container: Element;
subscribe: (callback: () => void) => () => void;
getSnapshot: () => Map<Element, ReactNode>;
onMounted: () => void;
LayoutComponent?: LayoutComponent;
ErrorBoundaryFallbackComponent?: ErrorBoundaryFallbackComponent;
}) {
const props = {
subscribe,
getSnapshot,
onMounted,
ErrorBoundaryFallback: ErrorBoundaryFallbackComponent || DefaultErrorBoundaryFallbackComponent,
};
const Layout = LayoutComponent || DefaultLayoutComponent;
const root = createReactRoot(container);
root.render(
<Layout>
<RootProvider {...props} />
</Layout>,
);
return () => root.unmount();
}

const DefaultLayoutComponent: LayoutComponent = StrictMode;
const DefaultErrorBoundaryFallbackComponent: ErrorBoundaryFallbackComponent = ({
error,
element,
}) => {
const message = element.getAttribute('fallback-message') ?? error.message;
return (
<div role="alert">
<pre style={{ color: 'red' }}>{message}</pre>
</div>
);
};

function RootProvider({
subscribe,
getSnapshot,
onMounted,
ErrorBoundaryFallback,
}: {
subscribe(callback: () => void): () => void;
getSnapshot(): Map<Element, ReactNode>;
onMounted: () => void;
ErrorBoundaryFallback: ErrorBoundaryFallbackComponent;
}) {
useEffect(onMounted, []);
const cache = useSyncExternalStore(subscribe, getSnapshot);

return (
<>
{...Array.from(cache).map(([element, content]) =>
createPortal(
<ErrorBoundary
fallbackRender={(props) => <ErrorBoundaryFallback element={element} {...props} />}
>
{content}
</ErrorBoundary>,
element,
getKeyForElement(element),
),
)}
</>
);
}

const keys = new WeakMap<Element, string>();

function getKeyForElement(element: Element): string {
let key = keys.get(element);
if (!key) {
key = Math.random().toString(36).slice(2);
keys.set(element, key);
}
return key;
}
9 changes: 1 addition & 8 deletions packages/react/src/root.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
import { encode as htmlEncode } from 'html-entities';
import { getByLabelText, getByText, getByRole, fireEvent, waitFor } from '@testing-library/dom';

import { createRoot, defaultSchema, type Manifest, type Observable } from '.';
import type { ReactComponent } from './react-tree-builder';
import { createRoot, defaultSchema, type Manifest, type Observable, type ReactComponent } from '.';

const NAME_ATTRIBUTE = defaultSchema.nameAttribute;
const PROPS_ATTRIBUTE = defaultSchema.propsAttribute;
Expand Down Expand Up @@ -70,7 +69,6 @@ describe('@coldwired/react', () => {
it('render simple fragment', async () => {
document.body.innerHTML = `<${DEFAULT_TAG_NAME}><div class="title">Hello</div></${DEFAULT_TAG_NAME}>`;
const root = createRoot({ loader: (name) => Promise.resolve(manifest[name]) });
await root.mount();
await root.render(document.body).done;

expect(document.body.innerHTML).toEqual(
Expand All @@ -91,7 +89,6 @@ describe('@coldwired/react', () => {
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(
Expand All @@ -109,7 +106,6 @@ describe('@coldwired/react', () => {
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(
Expand All @@ -136,7 +132,6 @@ describe('@coldwired/react', () => {
let 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(
Expand Down Expand Up @@ -172,7 +167,6 @@ describe('@coldwired/react', () => {
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(
Expand Down Expand Up @@ -206,7 +200,6 @@ describe('@coldwired/react', () => {
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(
Expand Down
Loading

0 comments on commit 4c47a36

Please sign in to comment.