-
-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
hydration of pre-rendered lazy route / component triggers exception, corrupts component tree #425
Comments
Using the web inspector / debugger, I have narrowed it down to the wmr/packages/preact-iso/lazy.js Line 11 in 928c7a0
|
If I comment out this wmr/packages/preact-iso/lazy.js Line 11 in 928c7a0
...then the exception occurs at the wmr/packages/preact-iso/router.js Lines 109 to 114 in 928c7a0
|
What is your method to debug into Preact's internals? (sourcemaps? if so, how to enable them in WMR's prerender build mode?) If I understand correctly, some code makes use of reserved minified properties (**), so aliasing
(**) For example wmr/packages/preact-iso/lazy.js Line 22 in 24417f6
|
Here is another data point to help troubleshoot this problem: The exception occurs as soon as the lazy / dynamically-imported component is loaded. By adding an artificial loading delay, and by navigating to a different router path during the load, it is possible to reliably reproduce a broken VNode/DOM tree (i.e. the lazy component is displayed at the same time as the new route). Once again, this only occurs in prerender / production mode ( const AboutLate = lazy(() => import('./pages/about/index.js')); ==> const AboutLate = lazy(
() =>
new Promise((resolve) => {
setTimeout(() => {
resolve(import('./pages/about/index.js'));
}, 2000);
}),
); |
In order to prevent the fatal error described in the message above (which really is a deal breaker for using async routes / lazy components at the moment in WMR), I am using the following workaround which prevents user attempts to trigger other routes while an async route is loading (i.e. lazy component not yet tree-mounted): EDIT: I use a CSS cursor "not allowed" on router links that do not respond to clicks whilst async route is loading, and I use a CSS cursor "wait" on the link that triggered the lazy component. useEffect(() => {
const clickHandler = (ev) => {
if (!ev.target) {
return;
}
const linkEl = ev.target.closest('a[href]');
if (!linkEl || linkEl.origin !== window.location.origin) {
return;
}
if (isLoading) {
ev.stopPropagation();
ev.preventDefault();
}
// this is just a handy feature to test full server reload (not necessary for this workaround)
if (ev.altKey) {
ev.stopPropagation(); // prevents preact-iso router (see comment above)
ev.preventDefault(); // prevents default linking / popup menu behaviour
window.location.href = linkEl.href;
}
};
document.addEventListener('click', clickHandler, {
capture: true,
});
return () => {
document.removeEventListener('click', clickHandler, {
capture: true,
});
};
}, [isLoading]); As the bug only occurs with pre-rendered static SSR ( As you can see, this technique relies on a boolean variable |
Here's how const ctx = {
routerLoading: (isLoading: boolean) => {
//
},
};
const ContextRouterLoading = createContext(ctx);
ContextRouterLoading.displayName = 'Router Loading Context'; export const App = () => {
const [isLoading, setLoading] = useState(false);
const ctx = {
routerLoading: (isLoading) => {
// https://github.com/preactjs/wmr/issues/425
if (!!document.querySelector('script[type=isodata]') // pre-rendered
&& !window.__LAZY_CHECK) {
window.__LAZY_CHECK = true;
if (isLoading) {
return;
}
}
setLoading(isLoading);
},
};
useEffect(() => {
const clickHandler = (ev) => {
if (!ev.target) {
return;
}
const linkEl = ev.target.closest('a[href]');
if (!linkEl || linkEl.origin !== window.location.origin) {
return;
}
if (isLoading) {
ev.stopPropagation();
ev.preventDefault();
return;
}
if (ev.altKey) {
ev.stopPropagation(); // prevents preact-iso router (see comment above)
ev.preventDefault(); // prevents default linking / popup menu behaviour
window.location.href = linkEl.href;
return;
}
};
document.addEventListener('click', clickHandler, {
capture: true,
});
return () => {
document.removeEventListener('click', clickHandler, {
capture: true,
});
};
}, [isLoading]);
return (
<ContextRouterLoading.Provider value={ctx}>
<LocationProvider>
<div>
<Header loading={isLoading} />
<ErrorBoundary
onError={(err) => {
console.log('ErrorBoundary onError: ', err);
}}
>
<Router
onLoadStart={
(url) => {
ctx.routerLoading(true);
}
}
onLoadEnd={
(url) => {
ctx.routerLoading(false);
}
}
>
<RouteWrapper path="/">
<Home />
</RouteWrapper>
<RouteWrapper path="/about-late">
<AboutLate />
</RouteWrapper>
<RouteWrapper default>
<NotFound />
</RouteWrapper>
</Router>
</ErrorBoundary>
</div>
</LocationProvider>
</ContextRouterLoading.Provider>
);
}; Finally, note that the function RouteWrapper(props) {
const [error, setError] = useState(undefined);
this.componentDidCatch = (err) => {
if (!err.then) {
setError(err);
}
};
if (error) {
return (
<>
<p>ERROR!</p>
<p>{error.message}</p>
<button
onClick={() => {
setError(undefined);
}}
>
Try again
</button>
</>
);
}
return props.children;
} I hope this helps :) |
Ah, another behaviour I've noticed is that the exception occurs shortly after hydration, but not instantaneously. There is a short time window during which the user can click on a routed link, resulting in corrupting the component tree. Thankfully that was easy to solve as I already have code in place to check hydration status. I just added a 500ms timeout to provide enough time for the exception to occur, beyond which point the other workarounds detailed above are effective. Phew! :) |
I have been running tests based on the stream of tweaks / fixes in preact-iso/router, notably: Unfortunately, I am still able to reproduce this bug consistently. Thankfully I am successfully using several workarounds to prevent the bug's occurrence, but the resulting code is quite convoluted. Just to be clear: the |
Here is another method to quickly reproduce the bug, from your local copy of the WMR repository:
|
This should have been fixed by #525. |
Describe the bug
Exception
TypeError: e.__k is null
is raised when attempting to hydrate a pre-rendered lazy route / dynamically-imported component. The error doesn't occur when hydrating a non-lazy route first, then navigating to a lazy one.To Reproduce
npm init wmr lazy-hydrate-error
cd lazy-hydrate-error
public/index.js
, addonError
prop e.g. :<ErrorBoundary onError={(e) => { console.log('ErrorBoundary onError: ', e); }}>
npm run build && npm run serve
http://192.168.1.127:8080/about
in web browser + open inspectorTypeError: e.__k is null
=> somewhere downstream of thesetState()
call chain ... seems to beupdate(1)
in thelazy
wrapper component:wmr/packages/preact-iso/lazy.js
Line 11 in 928c7a0
Here is another method to quickly reproduce the bug, from your local copy of the WMR repository:
main
branch, runyarn demo serve
http://localhost:8080/lazy-and-late
http://localhost:8080/lazy-and-late
withCMD
+R
to start over againExpected behavior
No error should occur.
Desktop (please complete the following information):
Additional context
This behaviour is problematic when needing to leverage
onError
to handle error state, see for example this related issue: #423The text was updated successfully, but these errors were encountered: