Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/react-from-markup/src/ILoadedOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface IOptions {
allSelectors: { [key: string]: string };
compoundSelector: string;
extra: object;
}

export default IOptions;
1 change: 1 addition & 0 deletions packages/react-from-markup/src/IOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
interface IOptions {
extra: object;
getQuerySelector?: (key: string) => string;
}

export default IOptions;
78 changes: 56 additions & 22 deletions packages/react-from-markup/src/rehydrator.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import domElementToReact from "dom-element-to-react";
import * as ReactDOM from "react-dom";

import ILoadedOptions from "./ILoadedOptions";
import IOptions from "./IOptions";
import IRehydrator from "./IRehydrator";

const rehydratableToReactElement = async (
el: Element,
rehydrators: IRehydrator,
options: IOptions
options: ILoadedOptions
): Promise<React.ReactElement<any>> => {
const rehydratorName = el.getAttribute("data-rehydratable");
const rehydratorSelector = Object.keys(options.allSelectors).find(selector =>
el.matches(selector)
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance consideration: This introduces a find loop on each call of rehydratableToReactElement.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth considering switch to for loop:

Source. Need more data.


if (!rehydratorSelector) {
throw new Error("No rehydrator selector matched the element.");
}

const rehydratorName = options.allSelectors[rehydratorSelector];

if (!rehydratorName) {
throw new Error("Rehydrator name is missing from element.");
Expand All @@ -31,13 +40,13 @@ const rehydratableToReactElement = async (

const createCustomHandler = (
rehydrators: IRehydrator,
options: IOptions
options: ILoadedOptions
) => async (node: Node) => {
// This function will run on _every_ node that domElementToReact encounters.
// Make sure to keep the conditional highly performant.
if (
node.nodeType === Node.ELEMENT_NODE &&
(node as Element).hasAttribute("data-rehydratable")
(node as Element).matches(options.compoundSelector)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance consideration: How does .matches compare to hasAttribute?

) {
return rehydratableToReactElement(node as Element, rehydrators, options);
}
Expand All @@ -61,7 +70,7 @@ const createReactRoot = (el: Node) => {
const rehydrateChildren = async (
el: Node,
rehydrators: IRehydrator,
options: IOptions
options: ILoadedOptions
) => {
const container = createReactRoot(el);

Expand Down Expand Up @@ -91,30 +100,55 @@ const render = ({
ReactDOM.render(rehydrated as React.ReactElement<any>, root);
};

const createQuerySelector = (rehydratableIds: string[]) =>
rehydratableIds.reduce(
(acc: string, rehydratableId: string) =>
`${acc ? `${acc}, ` : ""}[data-rehydratable*="${rehydratableId}"]`,
const defaultGetQuerySelector = (key: string) =>
`[data-rehydratable*="${key}"]`;

const createQuerySelectors = (
rehydratableIds: string[],
getQuerySelector: ((key: string) => string) = defaultGetQuerySelector
) => {
const allSelectors: { [key: string]: string } = rehydratableIds.reduce(
(acc, key) => ({ ...acc, [getQuerySelector(key)]: key }),
{}
);

const compoundSelector = Object.keys(allSelectors).reduce(
(acc: string, selector: string) => `${acc ? `${acc}, ` : ""}${selector}`,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance consideration: Additional loop on each hydrate, but only called once per hydrate.

""
);

return {
allSelectors,
compoundSelector
};
};

export default async (
container: Element,
rehydrators: IRehydrator,
options: IOptions
) => {
const selector = createQuerySelector(Object.keys(rehydrators));

const roots = Array.from(
// TODO: allow setting a container identifier so multiple rehydration instances can exist
container.querySelectorAll(selector)
).reduce((acc: Element[], root: Element) => {
// filter roots that are contained within other roots
if (!acc.some(r => r.contains(root))) {
acc.push(root);
}
return acc;
}, []);
const { allSelectors, compoundSelector } = createQuerySelectors(
Object.keys(rehydrators),
options.getQuerySelector
);

const loadedOptions: ILoadedOptions = {
allSelectors,
compoundSelector,
extra: options.extra
};

const roots = Array.from(container.querySelectorAll(compoundSelector)).reduce(
(acc: Element[], root: Element) => {
// filter roots that are contained within other roots
if (!acc.some(r => r.contains(root))) {
acc.push(root);
}
return acc;
},
[]
);

// TODO: solve race condition when a second rehydrate runs

Expand All @@ -128,7 +162,7 @@ export default async (
const {
container: rootContainer,
rehydrated
} = await rehydrateChildren(root, rehydrators, options);
} = await rehydrateChildren(root, rehydrators, loadedOptions);

return { root: rootContainer, rehydrated };
} catch (e) {
Expand Down