Skip to content

Commit

Permalink
fix(#150): improve hydration props
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Apr 11, 2021
1 parent 95357a0 commit ad80581
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 45 deletions.
8 changes: 4 additions & 4 deletions examples/partial-hydration/src/components/Clock.tsx
Expand Up @@ -2,10 +2,10 @@ import { FunctionalComponent } from "preact";
import { useEffect, useState } from "preact/hooks";
import { withHydrate } from "microsite/hydrate";

const Clock: FunctionalComponent<{ initialDate: string }> = ({
initialDate,
}) => {
const [date, setDate] = useState(initialDate);
const Clock: FunctionalComponent<{ initialDate: Date }> = ({ initialDate }) => {
const [date, setDate] = useState(
(initialDate || new Date()).toLocaleString().replace(", ", " at ")
);

useEffect(() => {
let id = setInterval(() => {
Expand Down
4 changes: 2 additions & 2 deletions examples/partial-hydration/src/components/Static/index.tsx
@@ -1,12 +1,12 @@
import { h, FunctionalComponent } from "preact";

const Static: FunctionalComponent<{ renderedAt: string }> = ({
const Static: FunctionalComponent<{ renderedAt: Date }> = ({
renderedAt,
children,
}) => {
return (
<section>
<h3>Page rendered on {renderedAt}</h3>
<h3>Page rendered on {(renderedAt || new Date()).toLocaleString()}</h3>

{children}

Expand Down
18 changes: 16 additions & 2 deletions examples/partial-hydration/src/pages/index.tsx
Expand Up @@ -18,7 +18,21 @@ const Index: FunctionalComponent<any> = ({ renderedAt }) => {

<main>
<Static renderedAt={renderedAt}>
<Clock initialDate={renderedAt} />
<Clock
initialDate={renderedAt}
autoplay
playsinline
sources={[
{
src: "/home/background-desktop.webm",
type: "video/webm",
},
{
src: "/home/background-desktop.mp4",
type: "video/mp4",
},
]}
/>
</Static>
<Idle />
<Visible />
Expand All @@ -37,7 +51,7 @@ export default definePage(Index, {
async getStaticProps() {
return {
props: {
renderedAt: new Date().toLocaleString().replace(", ", " at "),
renderedAt: new Date(),
},
};
},
Expand Down
21 changes: 11 additions & 10 deletions packages/microsite/assets/microsite-runtime.js
Expand Up @@ -34,18 +34,23 @@ const createObserver = (hydrate) => {
return io;
};

function attach(fragment, data, { key, name, source }, cb) {
const { p: { children = null, ...props } = {}, m: method = "idle", f: flush } = data;
function attach(fragment, data, { name, source }, cb) {
const {
p: propKey,
m: method = "idle",
f: flush,
} = data;

const hydrate = async () => {
if (window.__MICROSITE_DEBUG)
console.log(`[Hydrate] <${key} /> hydrated via "${method}"`);
const { [name]: Component } = await import(source);

const props = window.__MICROSITE_PROPS[propKey] || {};

if (flush) {
render(h(Component, props, children), fragment);
render(h(Component, props), fragment);
} else {
rehydrate(h(Component, props, children), fragment);
rehydrate(h(Component, props), fragment);
}
if (cb) cb();
};
Expand Down Expand Up @@ -108,11 +113,7 @@ function parseHydrateBoundary(node) {
let result = ATTR_REGEX.exec(text);
while (result) {
let [, attr, val] = result;
if (attr === "p") {
props[attr] = JSON.parse(val);
} else {
props[attr] = val;
}
props[attr] = val;
result = ATTR_REGEX.exec(text);
}
return props;
Expand Down
30 changes: 30 additions & 0 deletions packages/microsite/package-lock.json

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

5 changes: 4 additions & 1 deletion packages/microsite/package.json
Expand Up @@ -46,7 +46,9 @@
"@prefresh/snowpack": "^3.1.1",
"@snowpack/plugin-dotenv": "^2.0.5",
"arg": "^5.0.0",
"astring": "^1.7.4",
"esbuild": "^0.9.5",
"estree-util-value-to-estree": "^1.2.0",
"execa": "^4.0.3",
"globby": "^11.0.1",
"kleur": "^4.1.3",
Expand All @@ -55,9 +57,10 @@
"path-browserify": "^1.0.1",
"polka": "^0.5.2",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.16",
"preact-render-to-string": "^5.1.19",
"rollup": "^2.32.1",
"rollup-plugin-styles": "^3.11.0",
"shorthash": "^0.0.2",
"sirv": "^1.0.10",
"snowpack": "^3.1.1"
},
Expand Down
33 changes: 28 additions & 5 deletions packages/microsite/src/cli/microsite-dev.tsx
Expand Up @@ -9,6 +9,7 @@ import { readDir } from "../utils/fs.js";
import { promises as fsp } from "fs";
import { ErrorProps } from "error.js";
import { loadConfiguration } from "../utils/command.js";
import { serializeToJsString } from "../utils/serialize.js";
import { h, FunctionalComponent } from "preact";
import {
generateStaticPropsContext,
Expand All @@ -23,6 +24,7 @@ let renderToString: any;
let csrSrc: string;
let Document: any;
let __HeadContext: any;
let __PageContext: any;
let __InternalDocContext: any;
let ErrorPage: any;
let errorSrc: string;
Expand Down Expand Up @@ -69,10 +71,12 @@ const renderPage = async (
exports: {
Document: InternalDocument,
__HeadContext: __Head,
__PageContext: __Page,
__InternalDocContext: __Doc,
},
} = await runtime.importModule(documentSrc);
__HeadContext = __Head;
__PageContext = __Page;
__InternalDocContext = __Doc;
try {
const {
Expand Down Expand Up @@ -152,24 +156,40 @@ const renderPage = async (
},
};

const pageContext = {
props: {
current: {},
},
};

const HeadProvider: FunctionalComponent = ({ children }) => {
return <__HeadContext.Provider value={headContext} {...{ children }} />;
};

const PageProvider: FunctionalComponent = ({ children }) => {
return <__PageContext.Provider value={pageContext} {...{ children }} />;
};

const { __renderPageResult, ...docProps } = await Document.prepare({
renderPage: async () => ({
__renderPageResult: renderToString(
<HeadProvider>
<Component {...pageProps} />
</HeadProvider>
<PageProvider>
<HeadProvider>
<Component {...pageProps} />
</HeadProvider>
</PageProvider>
),
}),
});

const docContext = {
dev: componentPath,
devProps: pageProps ?? {},
devProps:
pageProps && Object.keys(pageProps).length > 0
? serializeToJsString(pageProps)
: "{}",
__csrUrl: csrSrc,
__renderPageProps: pageContext.props.current,
__renderPageHead: headContext.head.current,
__renderPageResult,
};
Expand Down Expand Up @@ -351,8 +371,11 @@ export default async function dev(
res.setHeader("Content-Type", result.contentType);

const MIME_EXCLUDE = ["image", "font"];
const isMeta = req.url.indexOf("/_snowpack/") !== -1;
const isMicrosite = req.url.indexOf("/microsite/") !== -1;
if (
req.url.indexOf("/_snowpack/pkg/microsite") === -1 &&
!isMeta &&
!isMicrosite &&
result.contentType &&
!MIME_EXCLUDE.includes(result.contentType.split("/")[0])
) {
Expand Down
30 changes: 25 additions & 5 deletions packages/microsite/src/document.tsx
Expand Up @@ -14,6 +14,10 @@ export const __HeadContext = createContext({
head: { current: [] },
});

export const __PageContext = createContext({
props: { current: {} },
});

/** @internal */
export const __InternalDocContext = createContext<any>({});

Expand Down Expand Up @@ -58,10 +62,12 @@ export const Html: FunctionalComponent<JSX.HTMLAttributes<HTMLHtmlElement>> = ({
...props
}) => <html lang={lang} dir={dir} {...props} />;

export const Main: FunctionalComponent<Omit<
JSX.HTMLAttributes<HTMLDivElement>,
"id" | "dangerouslySetInnerHTML" | "children"
>> = (props) => {
export const Main: FunctionalComponent<
Omit<
JSX.HTMLAttributes<HTMLDivElement>,
"id" | "dangerouslySetInnerHTML" | "children"
>
> = (props) => {
const { __renderPageResult } = useContext(__InternalDocContext);
return (
<div
Expand Down Expand Up @@ -128,6 +134,7 @@ export const Head: FunctionalComponent<JSX.HTMLAttributes<HTMLHeadElement>> = ({
export const MicrositeScript: FunctionalComponent = () => {
const {
__csrUrl,
__renderPageProps,
debug,
hasGlobalScript,
basePath,
Expand All @@ -136,6 +143,8 @@ export const MicrositeScript: FunctionalComponent = () => {
devProps,
} = useContext(__InternalDocContext);

const propsMap = __renderPageProps ? Object.entries(__renderPageProps) : [];

return (
<Fragment>
{dev && (
Expand All @@ -152,7 +161,7 @@ export const MicrositeScript: FunctionalComponent = () => {
dangerouslySetInnerHTML={{
__html: `import csr from '${__csrUrl}';
import Page from '${dev}';
csr(Page, ${JSON.stringify(devProps)});`,
csr(Page, ${devProps});`,
}}
/>
<script
Expand Down Expand Up @@ -184,6 +193,17 @@ csr(Page, ${JSON.stringify(devProps)});`,
}}
/>
)}
{scripts && propsMap.length > 0 && (
<script
type="module"
async
dangerouslySetInnerHTML={{
__html: `window.__MICROSITE_PROPS = {${propsMap
.map(([hash, props]) => `"${hash}":${props}`)
.join(", ")}}`,
}}
/>
)}
{scripts && (
<script
type="module"
Expand Down
41 changes: 32 additions & 9 deletions packages/microsite/src/hydrate.tsx
@@ -1,5 +1,7 @@
import { h, FunctionComponent, createContext, VNode } from "preact";
import { useContext } from "preact/hooks";
import { serializeToJsString, hashString } from "./utils/serialize.js";
import { __PageContext } from "./document.js";

const isServer = typeof window === "undefined";
export const HydrateContext = createContext<string | false>(false);
Expand All @@ -14,11 +16,14 @@ export function withHydrate<T extends FunctionComponent<any>>(
Component: T,
hydrationProps: HydrationProps = {}
): T {
const name = hydrationProps.displayName || Component.displayName || Component.name;
const name =
hydrationProps.displayName || Component.displayName || Component.name;
const { method, fallback: Fallback } = hydrationProps;

const Wrapped: FunctionComponent<any> = (props, ref) => {
const hydrateParent = useContext(HydrateContext);
const pageCtx = useContext(__PageContext);
const hasProps = Object.keys(props).length > 0;
if (hydrateParent)
throw new Error(
`withHydrate() should only be called at the top-level of a Component tree. <${name} /> should not be nested within <${hydrateParent} />`
Expand All @@ -29,16 +34,34 @@ export function withHydrate<T extends FunctionComponent<any>>(
`withHydrate() is unable to serialize complex \`children\`. Please inline these children into <${name} />.`
);

const p = isServer ? `p=${JSON.stringify(props)}` : '';
const m = isServer && method ? `m=${method}` : '';
const f = isServer && typeof Fallback !== 'undefined' ? 'f=1' : '';
const Marker = 'hydrate-marker' as any;
const Placeholder = 'hydrate-placeholder' as 'div';
const serialized = hasProps ? serializeToJsString(props) : "";
const hash = hasProps ? hashString(serialized) : "";
if (hasProps) {
pageCtx.props.current[hash] = serialized;
}

const p = isServer && hasProps ? `p=${hash}` : "";
const m = isServer && method ? `m=${method}` : "";
const f = isServer && typeof Fallback !== "undefined" ? "f=1" : "";
const Marker = "hydrate-marker" as any;
const Placeholder = "hydrate-placeholder" as "div";
return (
<HydrateContext.Provider value={name}>
{isServer && (<Marker dangerouslySetInnerHTML={{ __html: `?h c=${name} ?` }} />)}
{typeof Fallback !== 'undefined' ? (Fallback || <Placeholder />) : <Component {...{ ...props, ref }} />}
{isServer && (<Marker dangerouslySetInnerHTML={{ __html: `?h ${[p, m, f].filter(v => v).join(' ')} ?` }} />)}
{isServer && (
<Marker dangerouslySetInnerHTML={{ __html: `?h c=${name} ?` }} />
)}
{typeof Fallback !== "undefined" ? (
Fallback || <Placeholder />
) : (
<Component {...{ ...props, ref }} />
)}
{isServer && (
<Marker
dangerouslySetInnerHTML={{
__html: `?h ${[p, m, f].filter((v) => v).join(" ")} ?`,
}}
/>
)}
</HydrateContext.Provider>
);
};
Expand Down

0 comments on commit ad80581

Please sign in to comment.