Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement: Support SSR-able components as props for hydrated components #138

Open
ratorx opened this issue Mar 13, 2021 · 7 comments
Open

Comments

@ratorx
Copy link
Contributor

ratorx commented Mar 13, 2021

I'm a big fan of partial hydration. But, I think the JS bundles can be made smaller. Right now, the code for all children of partially rendered components has to be sent to the client, even if it's not actually required.

Starting example

const Toggle = () => {
	state, setState = useState(false)
	return <button onClick={() => setState((state) => !state)}><FontAwesomeIcon icon={state ? faCalendar : faClock} /></button>
}

In this case FontAwesomeIcon is an example component that needs no interactivity. It is just a helper component to generate the SVG icon given a name.

If it was written like that, hydrating it requires the entire library (in this case react-fontawesome) to be bundled. However, the toggle uses only 2 icons, which could easily be SSR rendered at build time. I don't think this is possible to detect automatically (for the same reason that withHydrate is opt-in), but it should be possible to refactor this to not bundle react-fontawesome.

Vague Implementation

const Toggle = (props: {on: SSRElement, off: SSRElement}) => {
	state, setState = useState(false)
	return <button onClick={() => setState((state) => !state)}>{state ? on : off}</button>

And it's called by passing in the rendered font awesome icons as a prop:

<Toggle on={<SSR><FontAwesomeIcon icon={faCalendar} /></SSR>} off={<SSR><FontAwesomeIcon icon={faClock} /></SSR>} />

where SSR and SSRElement would be provided as utilities by this project.

Details

The tricky bit is the implementation of SSR and SSRElement. A naive one might be for SSR to render its child to an SSRElement (which is a light wrapper around an HTML string) and then for SSRElement to dangerouslysetinnerhtml to the HTML string on the client.

Do you think something like this could work? Would this be an useful feature for this project (considering the focus on minimising runtime javascript)?

@ratorx
Copy link
Contributor Author

ratorx commented Mar 14, 2021

I thought about this some more and refined the idea a bit. What this boils down to is to support components as props to partially hydrated components. Not all components can be passed in as props, only those that don't need to be interactive. These can be marked by a special HOC.

withHydrate will allow props and children which are marked by the special HOC. For these components, it will:

  • Render them to an HTML string
  • Encode it with something like base64 (otherwise the hydration marker breaks)
  • Add a special JSON object to the hydration marker (something like {"content": base64 encoded html, "__microsite_component": true}).

On the client, the hydration logic will:

  • Identify special JSON object
  • Decode it to HTML
  • Parse them into a VDOM/simple component (with something like preact-markup) (this is optional, the alternative is just to use dangerouslysetinnerhtml)
  • Pass them as arguments to the hydrated component

This lets you write normal components for partial hydration and have component props "magically work" to some extent. Obviously it's a bit complex and it would possibly introduce a new dependency (preact-markup), but it would remove the restriction that props to hydrated components need to be simple JSON.

This would remove the 3rd caveat from the Automatic Partial Hydration docs (to some extent).

@ratorx ratorx changed the title Enhancement: SSR rendered children for partially hydrated components Enhancement: Support SSR-able components as props for hydrated components Mar 14, 2021
@natemoo-re
Copy link
Owner

This is a very awesome idea! I'm thinking this over, but your proposed implementation sounds right to me. I will start kicking the tires on this one 😄

@ratorx
Copy link
Contributor Author

ratorx commented Mar 21, 2021

I tried out some of the encoding ideas for a proof-of-concept. Not really in a shareable format yet, but I'll try and setup a branch or something. A few extra notes:

  • The special encoding/decoding is actually really easy because of JSON reviver/replacer. For the encoding, I ended up not using a special HOC but rather just using the components directly and using Preact.isValidElement to decide when to encode to base64 (and add the __microsite_component tag).
  • This needs an extra dependency in microsite runtime. AFAIK it's not possible to use dangerouslysetinnerhtml on a fragment (which means that the parsed HTML will always end up with a wrapper element). So the runtime will either need a dependency like react-dom-fragment or preact-markup to make this work. I think preact-markup is the neater option, especially since it allows the VDOM to be updated incrementally.
  • If the component being encoded contains a withHydrate anywhere inside it, there's the potential of weird things happening (possibly, haven't properly tested it). It doesn't find the parent hydrated component (since the rendering happens out-of-order). There needs to be some logic to detect withHydrate used in component props and error on it. An easy way to detect this might be to wrap component props with a HydrationContext before rendering it to a string. Then nested withHydrate can be detected in the same way as it currently is.

@ratorx
Copy link
Contributor Author

ratorx commented Mar 21, 2021

I created #148 for something tangentially related to the third point. Even if you choose to do it, using withHydrate inside a component prop should be an error, because there is no chance that the prop is hydrated (since it is statically rendered on the server and passed as plain HTML), so the code will need a special case for that.

@natemoo-re
Copy link
Owner

Nice! Excited to see what you've been playing with.

One other idea I've been thinking about in regard to optimizing hydration is somehow replacing any non-hydrated components with a noop component using Preact's shouldComponentUpdate to opt out of any DOM mutations for static components. Preact's hydrate function does not try to reconcile the DOM so what was SSR'd won't be wiped out.

The client would render the entire component tree like a typical Preact app, but all static stuff would be removed via treeshaking. That would have the added benefit of all context working normally and not needing to worry about nested hydration. The withHydrate HOC would only instruct Microsite not to convert a given component to a noop. Still need to explore how lazy hydration would work, though!

@ratorx
Copy link
Contributor Author

ratorx commented Mar 24, 2021

I like the tree-shaking idea as a different way to do hydration with less caveats. However, I don’t think it solves this particular problem.

In this case, I want to avoid hydrating children of a hydrated component. I’m not entirely sure how that would work with the tree shaking idea. I don’t think they are mutually exclusive, but at some point I think the work to serialise/deserialise children would have to be done (with the alternative being to not support it, which would be a shame).

I guess in that world, the default could be to treat children as rich components and have Preact hydrate them normally and introduce a HOC/wrapper to mark them as fully SSR?

@ratorx
Copy link
Contributor Author

ratorx commented Mar 25, 2021

I've made a minimal (hopefully working) PoC here.

It currently doesn't work because I can't figure out how to pass the new dependency (preact-markup) to microsite-runtime.js. But this is the implementation other than that (I tested it without preact-markup, by just using a wrapper div)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants