natemoo-re / microsite

Do more with less JavaScript. Microsite is a smarter, performance-obsessed static site generator powered by Preact and Snowpack.

Home Page:https://npm.im/microsite

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Hydrated component isn't initialized in prod builds when it is exported/imported under a name different from the name of the component it decorates

nikita-graf opened this issue · comments

A bit cryptic title but let me explain what this is about :)

Currently, in production mode code assumes that the name of the component that the withHydrate hook decorates and the name under which the decorated component is imported into the page are the same which is not always true.

The withHydrate hook when renders HTML stores the name of the component it decorates as a marker. The hydration code then reads the marker and tries to locate a component with this name among hydration bindings. Hydration bindings are determined based on named exports of the component file or default import names in the page component.

There are two cases when this logic fails.

A named export case:

// hydrated-component.tsx
export const HydratedComponent = withHydrate(HydratedComponentInternal);

// page.tsx
import {HydratedComponent} from './hydrated-component.tsx';

Default import case:

// hydrated-component.tsx
export default withHydrate(HydratedComponentInternal);

// page.tsx
import HydratedComponent from './hydrated-component.tsx';

In both cases in prod, the hydration code would try to locate the HydratedComponentInternal in the binding but this attempt would fail as there would be only single binding for the HydratedComponent.

Fixing this turned out to be tricky. The only fix I can think about is to parse hydration chunks, determine exported component names, and prefill a displayName parameter of the withHydrate hook with the corresponding names. This solution feels a bit hacky.

I'm wondering if you have any thoughts on what would be the right way to fix it. Take a look when you have time =)

Adding a repro below.

Steps to reproduce

  1. Checkout a named-export-fix branch in this repository.
  2. Run npm run serve
  3. Open http://localhost:8888/

Expected behavior

The page renders, HydratedComponentA and HydratedComponentB get hydrated and two alert messages are shown.

Actual behavior

The page renders but no components are hydrated because of a runtime js error.

I encountered the same issue the other day! This may not be documented yet, but it has a workaround as of #140: you can specify a displayName in the hydration options (my bad for not including a documentation update when I did the PR).

Edit: I also should have read further to see you already found that workaround. I agree it's hacky, but unfortunately there are some language limitations involved. The name inference for components is determined by either an explicit non-anonymous function name (which may also be a solution, but I didn't try) or the runtime's inference of assignment to a binding (var/let/const) of an anonymous function (either unnamed function or arrow).

Thanks for looking! Some documentation would definitely be helpful here!

What are your thoughts on prefilling the displayName parameter with the name of an exported hydrated component? Here is a quick prototype I made: link.

Unlikely it can be merged in the current state but maybe it's worth discussing it as a potential option? Maybe instead of prefilling the displayName , the same value could be provided as a static field of the component to be decorated.

Imagine if we had this content in a hydration chunk:

const Component = () => <div>...</div>;
export const HydratedComponent = withHydrate(Component);

it could be transformed to this for prod:

const Component = () => <div>...</div>;
Component .hydrationComponentName = 'HydratedComponent';
export const HydratedComponent = withHydrate(Component);

It still feels hacky (and a bit complex to implement) but the upside is that end-users likely won't run into issues with wrong component naming. Let me know what you think! 🙂

I agree that defaulting to the exported name would be ideal, as that's basically standard React/Preact behavior and what most users would expect. I'll defer to @natemoo-re on implementation preference, I know there's already some source transformation behavior in the build and IIRC I think I spotted some recent changes. I'll hold off on documentation until there's a general sense of the longer term design, as the current solution is definitely more of a hack than what you've proposed.

Thanks for all the ideas! I wonder if all doing all that extra annotation work or even requiring a manual displayName is really necessary? We already know the source module of the component! When we dynamically import it, we could just loop through all of the exports to find all the exported Preact components, then find the withHydrate component which has the inner component's name || displayName hoisted up? The name hoisting is already part of the server-side HOC but is stripped away on the client. I think we'd just need the client to have the same name hoisting logic and maybe a static __isWithHydrate flag?

As for performance, the entire module is loaded and parsed anyway even if you're only grabbing a single export, so looping through the exports should be negligible.

That's an interesting idea! It should work! Adding __isWithHydrate and name hoisting should be straightforward.

The most significant change that would be needed is to include inner component names into the hydrateBindings hash so that the runtime part would know a link to the file with the component that it wants to hydrate. Currently, the hydrateBindings contains exported names of the components as they appear in the source code and not the inner component names.

Maybe you see an easier way how runtime could locate a hydration chunk of an SSR rendered component?

A random idea: we could possibly add a hashed URL of the chunk next to other metadata that the withHydration renders in the SSR mode. Though, this would require modifying the AST just like in my pr linked above.

Turns out traversing the AST was necessary to map the internal component name back to the exported name! Take a look at what I came up with in #149. Seems to be working well 😄

ezoic increase your site revenue