Skip to content

Instantly share code, notes, and snippets.

@jarkkosyrjala
Created October 15, 2020 20:00
Show Gist options
  • Save jarkkosyrjala/cac1b827a52d210dac983745c4f21861 to your computer and use it in GitHub Desktop.
Save jarkkosyrjala/cac1b827a52d210dac983745c4f21861 to your computer and use it in GitHub Desktop.
Render static content as HTML and get the content from server side rendered HTML back as props before hydration
import * as React from 'react'
import DynamicAppWithStaticSections, {
DynamicAppWithStaticSectionsProps,
DynamicAppWithStaticSectionsHydrationProps,
} from './DynamicAppWithStaticSections'
import { hydrate } from 'react-dom'
interface AppWindow extends Window {
__APP__: DynamicAppWithStaticSectionsHydrationProps
}
declare const window: AppWindow
const partiallyStaticComponentProps: DynamicAppWithStaticSectionsProps = {
// Only part of the props are passed to JSON in DOM
...window.__APP__,
// Get the rendered html and pass it back as props
staticSections: Array.from(
document.querySelectorAll('.static-sections > section > div'),
(div) => div.innerHTML,
),
}
hydrate(
<DynamicAppWithStaticSections {...partiallyStaticComponentProps} />,
document.getElementById('app'),
)
import * as React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { useState } from 'react'
export interface DynamicAppWithStaticSectionsProps {
dynamicText: string
staticSections: string[]
}
export type DynamicAppWithStaticSectionsHydrationProps = Omit<
DynamicAppWithStaticSectionsProps,
'staticSections'
>
interface SectionWithStaticContentProps {
html: string
}
const SectionWithStaticContent: React.FC<SectionWithStaticContentProps> = ({ html }) => {
const [show, setShow] = useState<boolean>(true)
return (
<section>
<button onClick={() => setShow(!show)}>Toggle</button>
<div
style={{ display: show ? 'block' : 'none' }}
dangerouslySetInnerHTML={{ __html: html }}
/>
}
</section>
)
}
const DynamicAppWithStaticSections: React.FC<DynamicAppWithStaticSectionsProps> = ({
dynamicText,
staticSections,
}) => {
return (
<>
<div>{renderToStaticMarkup(<>{dynamicText}</>)}</div>
<div className="static-sections">
{staticSections.map((html) => (
<SectionWithStaticContent html={html} />
))}
</div>
</>
)
}
export default DynamicAppWithStaticSections
import * as React from 'react'
import { DynamicAppWithStaticSectionsProps } from './DynamicAppWithStaticSections'
export interface HtmlPageProps {
partiallyStaticComponent: DynamicAppWithStaticSectionsProps
}
/**
* Html template that prints the hydrations props as JSON
*/
const Html: React.FC<HtmlPageProps> = ({ partiallyStaticComponent, children }) => {
let { staticSections: _omit, ...hydrationProps } = partiallyStaticComponent
return (
<html>
<body>
<h1>Example</h1>
<div id="app">{children}</div>
<script
dangerouslySetInnerHTML={{
__html: `window.__APP__ = ${JSON.stringify(hydrationProps)}`,
}}
/>
</body>
</html>
)
}
export default Html
import { NextFunction, Request, Response } from 'express-serve-static-core'
import * as React from 'react'
import DynamicAppWithStaticSections, {
DynamicAppWithStaticSectionsProps,
} from './DynamicAppWithStaticSections'
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
import Html from './Html'
const getPartiallyStaticComponentProps = async (): Promise<DynamicAppWithStaticSectionsProps> => {
// Fetch from server etc...
return Promise.resolve({
dynamicText: 'foo',
staticSections: ['Hello', 'World'],
})
}
const exampleExpressRouteHandler = async (
_req: Request,
res: Response,
_next: NextFunction,
): Promise<any> => {
// get the props from somewhere
const partiallyStaticComponentProps = await getPartiallyStaticComponentProps()
return res.send(
renderToString(
<Html partiallyStaticComponent={partiallyStaticComponentProps}>
<DynamicAppWithStaticSections
{...{
...partiallyStaticComponentProps,
staticSections: partiallyStaticComponentProps.staticSections.map((content) =>
renderToStaticMarkup(<div className="fancy-but-static-sub-component">{content}</div>),
),
}}
/>
</Html>,
),
)
}
@jarkkosyrjala
Copy link
Author

The way hydration works is that the props need to be embedded to page as JSON so that it can be hydrated (or in some cases the content is in separate json file that is being loaded before hydration).

In a content heavy page this kind of JSON string can basically double the weight of a HTML file being served. Basically the content is printed twice: first as HTML and then the raw data as JSON.

Instead of printing the content twice, wouldn't it make sense to reuse the server side rendered html for hydration and get it back from the element.innerHTML?

@jarkkosyrjala
Copy link
Author

Of course when rendering purely static content then the component does not need to be hydrated at all. However, when ever there's anything interactive then hydration is needed and for hydration to work without re-render, the props need to match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment