-
-
Notifications
You must be signed in to change notification settings - Fork 108
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
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
https://github.com/preactjs/preact/blob/7556b61bd0d9f72bbd906c3abaebf2b1c9aeacbf/mangle.json#L40
|
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: