Skip to main content

React SSR in a Worker runtime

How Keywork uses React to render HTML

When a route handler responds to a request with a React component, Keywork automatically converts the content into a streamed response.

For example, consider this React Page component:

/shared/components/Page.tsx
import React from 'react'

export interface PageProps {
renderedAt: string
}

export const Page: React.FC<PageProps> = (props) => {
const renderedDate = new Date(props.renderedAt)

const showAlert = useCallback(() => {
alert('I was rendered on the server!')
}, [])

return (
<div>
<h1>Hello from React</h1>
<h1>Rendered at {renderedDate.toLocaleTimeString()}</h1>
<button onClick={showAlert}>Click me!</button>
</div>
)
}

...And how the component is then rendered by RequestRouter:

worker/worker.tsx
import React from 'react'
import { Page, PageProps } from '@local/shared/components/Page'
import { RequestRouter } from 'keywork/router'

export const router = new RequestRouter()

router.get('/', () => {
return <Page renderedDate={new Date()}>
})

Finally, the browser receives the response with a full HTML document:

<!DOCTYPE html>
<html>
<!-- <head .../> -->
<body>
<div>
<h1>Hello from React</h1>
<h1>Rendered at 11:45:32 AM</h1>

<button>Click me!</button>
</div>
</body>
</html>

Why hydration is necessary for dynamic React apps

However, if the React component has any dynamic behaviors, such as setting up event listeners, or data fetching, the browser must perform an additional rendering step called "hydration."

Hydration is how React revives the existing HTML markup sent by Keywork, almost as if the components were actually rendered in the browser.

In the example above, our button rendered just fine. But when clicked, the component doesn't invoke its showAlert callback as expected.

This is often where developers find themselves overburdened with complexity. We need to hydrate the React components, and provide the original PageProps passed to the Page component. And if you're used to your React framework handling this for you, like in Next.js, migrating your app to Cloudflare Workers may seem impossible.

Keywork does all the heavy lifting 😮‍💨

Serializing our static props

Before rendering your React component, Keywork will serialize any props passed as JSON, and inject them in the HTML response:

<body>
<!-- ... -->
<script id=":KeyworkSSRPropsElement:" type="text/javascript">
;(function () {
const encoded = '%7B%22renderedAt%22%3A%222022-07-05T19%3A18%3A36.320Z%22%7D'
self[':KeyworkSSRProps:'] = JSON.parse(decodeURIComponent(encoded))
})()
</script>
</body>
danger

When a route handler responds to a request using a React component, the props must be both serializable into JSON and deserialize back to their original shape:

  • Use null instead of undefined for optional values
  • Avoid circular references

Putting it all together

With our props available, all that's left to do is hydrate the app:

/browser/src/main.tsx
import { Page } from '@local/shared/components/Page'
import { KeyworkApp } from 'keywork/react/browser'
import { waitUntilDOMReady } from 'keywork/timers/browser'
import React from 'react'

waitUntilDOMReady().then(() => {
const app = new KeyworkApp()

return app.hydrate((staticProps) => (
<React.StrictMode>
<Page {...staticProps} />
</React.StrictMode>
))
})