Skip to content

Commit

Permalink
Fix serializations
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed May 12, 2024
1 parent 8d12d8e commit b2020f7
Show file tree
Hide file tree
Showing 8 changed files with 64 additions and 61 deletions.
8 changes: 8 additions & 0 deletions .changeset/tender-trees-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@quilted/react-query': patch
'@quilted/preact-browser': patch
'@quilted/browser': patch
'@quilted/quilt': patch
---

Fix serialization in edge cases where scripts load before DOMContentLoaded
54 changes: 10 additions & 44 deletions integrations/react-query/source/ReactQueryContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {RenderableProps, ComponentType} from 'preact';
import {Suspense} from 'preact/compat';
import {
dehydrate,
QueryClient,
Expand Down Expand Up @@ -34,49 +33,16 @@ export function ReactQueryContext({
<HydrationBoundary state={dehydratedState}>
{children as any}
</HydrationBoundary>

{/**
* This must run after any children that perform queries on the server. The
* `Serializer` component will wait until any pending queries are resolved,
* and once resolved, will add a serialization for the contents of the
* React Query client. We wrap this process in a Suspense boundary so that
* it just makes the server wait, but does not force any parent components to
* re-render once the suspense promise resolves.
*/}
<Suspense fallback={null}>
<Serializer client={client} />
</Suspense>
{typeof document === 'undefined' && (
<Serialize
id={SERIALIZATION_ID}
value={() =>
dehydrate(client, {
shouldDehydrateQuery: () => true,
})
}
/>
)}
</QueryClientProvider>
);
}

function Serializer({client}: {client: QueryClient}) {
if (typeof document === 'object') return null;

const promises: Promise<any>[] = [];

for (const query of client.getQueryCache().getAll()) {
const {state, options, meta} = query;

if (state.status === 'success' || state.status === 'error') continue;
if ((options as any).enabled === false) continue;
if (meta != null && meta.server === false) continue;

promises.push(query.fetch());
}

if (promises.length > 0) {
throw Promise.all(promises).then(() => {});
}

return (
<Serialize
id={SERIALIZATION_ID}
value={() =>
dehydrate(client, {
shouldDehydrateQuery: () => true,
})
}
/>
);
}
4 changes: 2 additions & 2 deletions packages/browser/source/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ export class BrowserElementAttributes<Element extends HTMLElement> {
export class BrowserSerializations {
private readonly serializations = new Map<string, unknown>(
Array.from(
document.querySelectorAll<HTMLMetaElement>(`meta[name^="serialized"]`),
document.querySelectorAll<HTMLMetaElement>(`meta[name^="serialized:"]`),
).map((node) => [
node.name.replace(/^serialized-/, ''),
node.name.replace(/^serialized:/, ''),
getSerializedFromNode(node),
]),
);
Expand Down
11 changes: 10 additions & 1 deletion packages/browser/source/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ export class BrowserResponseHeadElements<
)[] = [];

get value() {
return this.elements.map(resolveSignalOrValue);
return this.elements.map((element) =>
resolveSignalOrValue<Partial<HTMLElementTagNameMap[Element]>>(element),
);
}

constructor(readonly selector: Element) {}
Expand Down Expand Up @@ -160,6 +162,13 @@ export class BrowserResponseElementAttributes<Attributes> {
}

export class BrowserResponseSerializations {
get value() {
return [...this.serializations].map(([id, value]) => ({
id,
value: (typeof value === 'function' ? value() : value) as unknown,
}));
}

constructor(private readonly serializations = new Map<string, unknown>()) {}

get(id: string) {
Expand Down
1 change: 1 addition & 0 deletions packages/preact-browser/source/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
} from './server/hooks/response-cookie.ts';
export {useResponseStatus} from './server/hooks/response-status.ts';
export {useSearchRobots} from './server/hooks/search-robots.ts';
export {useResponseSerialization} from './server/hooks/serialized.ts';
export {useStrictTransportSecurity} from './server/hooks/strict-transport-security.ts';
export {useViewport} from './server/hooks/viewport.ts';

Expand Down
21 changes: 12 additions & 9 deletions packages/preact-browser/source/server/components/Serialize.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
export function Serialize<T>({id, value}: {id: string; value: T | (() => T)}) {
return (
<meta
name={`serialized-${id}`}
content={JSON.stringify(
typeof value === 'function' ? (value as any)() : value,
)}
/>
);
import {useResponseSerialization} from '../hooks/serialized.ts';

export function Serialize<T = unknown>({
id,
value,
}: {
id: string;
value: T | (() => T);
}) {
if (typeof document === 'object') return null;
useResponseSerialization(id, value);
return null;
}
13 changes: 13 additions & 0 deletions packages/preact-browser/source/server/hooks/serialized.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {useBrowserResponseAction} from './browser-response-action.ts';

/**
* Sets a serialization for the HTML response. This value can then be read using
* the `useSerialization` hook.
*/
export function useResponseSerialization(key: string, value: unknown) {
if (typeof document === 'object') return;

useBrowserResponseAction((response) => {
response.serializations.set(key, value);
});
}
13 changes: 8 additions & 5 deletions packages/quilt/source/server/request-router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {isValidElement, type VNode} from 'preact';
import {isValidElement, type VNode, type JSX} from 'preact';
import {
renderToStaticMarkup,
renderToStringAsync,
Expand Down Expand Up @@ -191,11 +191,14 @@ export async function renderToResponse(
{browserResponse.title.value && (
<title>{browserResponse.title.value}</title>
)}
{browserResponse.links.value.map((link, index) => (
<link key={index} {...link} />
{browserResponse.links.value.map((link) => (
<link {...(link as JSX.HTMLAttributes<HTMLLinkElement>)} />
))}
{browserResponse.metas.value.map((meta, index) => (
<meta key={index} {...meta} />
{browserResponse.metas.value.map((meta) => (
<meta {...(meta as JSX.HTMLAttributes<HTMLMetaElement>)} />
))}
{browserResponse.serializations.value.map(({id, value}) => (
<meta name={`serialized:${id}`} content={JSON.stringify(value)} />
))}
{synchronousAssets?.scripts.map((script) => (
<ScriptAsset
Expand Down

0 comments on commit b2020f7

Please sign in to comment.