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 0a408eb commit a9bbfa4
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 37 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
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,10 +57,11 @@
"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",
"recast": "^0.20.4",
"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
14 changes: 11 additions & 3 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 @@ -10,8 +12,6 @@ export interface HydrationProps {
fallback?: VNode<any> | null;
}

const encode = (str: string) => Buffer.from(str).toString("base64");

export function withHydrate<T extends FunctionComponent<any>>(
Component: T,
hydrationProps: HydrationProps = {}
Expand All @@ -21,6 +21,8 @@ export function withHydrate<T extends FunctionComponent<any>>(

return (function (props: any, ref: any) {
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. <${innerName} /> should not be nested within <${hydrateParent} />`
Expand All @@ -31,7 +33,13 @@ export function withHydrate<T extends FunctionComponent<any>>(
`withHydrate() is unable to serialize complex \`children\`. Please inline these children into <${innerName} />.`
);

const p = isServer ? `p=${encode(JSON.stringify(props))}` : "";
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;
Expand Down
13 changes: 5 additions & 8 deletions packages/microsite/src/runtime/index.tsx
Expand Up @@ -37,7 +37,7 @@ const createObserver = (hydrate) => {

function attach(fragment, data, { name, source }, cb) {
const {
p: { children = null, ...props } = {},
p: propKey,
m: method = "idle",
f: flush,
} = data;
Expand All @@ -46,11 +46,12 @@ function attach(fragment, data, { name, source }, cb) {
if (win.__MICROSITE_DEBUG)
console.log(`[Hydrate] <${name} /> 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 @@ -110,11 +111,7 @@ function parseHydrateBoundary(node) {
let result = ATTR_REGEX.exec(text);
while (result) {
let [, attr, val] = result;
if (attr === "p") {
props[attr] = JSON.parse(atob(val));
} else {
props[attr] = val;
}
props[attr] = val;
result = ATTR_REGEX.exec(text);
}
return props;
Expand Down

0 comments on commit a9bbfa4

Please sign in to comment.