solid: SSR: Cannot read properties of undefined (reading 'cloneNode') because getNextElement() is called without arguments
Describe the bug
Hi,
I’m working on getting SSR and hydration to work… I’m not using solid-start because I have my own simple project for doing SSG only and for learning purposes. I started over and over again with like 4 to 5 attempts but always end up with the same error and now I think it’s less likely that it’s only me doing something wrong here…?
What I actually do is building and bundling code using esbuild, and of course with a Babel approach using esbuild-plugin-solid to make sure the JSX and SSR/DOM code generation works correctly… (the dependency uses standard solid babel transform under the hood, see: https://github.com/amoutonbrady/esbuild-plugin-solid/blob/master/src/plugin.ts#L5)
For the server code bundle I have something like this:
import { build } from 'esbuild'
import { solidPlugin } from 'esbuild-plugin-solid'
// shortened and simplified
await build({
bundle: true,
format: 'esm',
platform: 'node',
// basePlugins has some more plugins for handling importing .css files etc., nothing that would deal with js/ts stuff
plugins: [solidPlugin({ generate: 'ssr', hydratable: true }), ...basePlugins],
})
For the client I build another code bundle:
// shortened and simplified
await build({
bundle: true,
platform: 'browser',
plugins: [solidPlugin({ generate: 'dom', hydratable: true }), ...basePlugins],
format: 'iife',
})
On the server I use the server code bundle and after executing the code below in a vm context:
Please note: The render() function here is not Solid’s render function. It is my own runtime render() function that, when executed on server side, calls renderToString() and on the client a different runtime impl. is executed and calls Solid’s hydrate().
PageScript is executed on server-side and injects the client-side code bundle (this is executed on server side) by returning a <script> tag pointing to that:
You can see the result of this down below in the generated HTML code.
SSR (renderToString()) works well, the HTML code generated is served, see below:
I was double-checking my code several times and checking against solid-ssr examples. I cannot find any big difference, except that I’m not using rollup, but esbuild – still the same Babel transform is used behind the scenes as part of esbuild-plugin-solid…
But then, on the client when my runtime calls,hydrate(fn, document) I always, whatever I do since months, run into:
index.ssg.js:1814 Uncaught TypeError: Cannot read properties of undefined (reading 'cloneNode')
at getNextElement (index.ssg.js:1814:24)
at index.ssg.js:1997:19
at index.ssg.js:2003:5
at index.ssg.js:679:58
at updateFn (index.ssg.js:49:42)
at runUpdates2 (index.ssg.js:373:21)
at createRoot2 (index.ssg.js:53:16)
at render2 (index.ssg.js:677:7)
at hydrate$1 (index.ssg.js:823:23)
at hydrate (index.ssg.js:1060:14)
And of course this is undefined because Solid generates the following, broken code:
// examples/homepage/src/pages/index.ssg.tsx
var _tmpl$ = /* @__PURE__ */ template(`<div>foobar</div>`, 2);
var _tmpl$2 = /* @__PURE__ */ template(`<script>console.log('test1')<\/script>`, 2);
var Body = () => [getNextElement(_tmpl$), getNextElement(_tmpl$2)];
var index_ssg_default = render(() => (() => {
const _el$3 = getNextElement(), _el$8 = getNextMatch(_el$3.firstChild, "body"), _el$9 = _el$8.nextSibling, [_el$10, _co$2] = getNextMarker(_el$9.nextSibling), _el$11 = _el$10.nextSibling, [_el$12, _co$3] = getNextMarker(_el$11.nextSibling);
createComponent(NoHydration, {});
insert(_el$8, createComponent(Body, {}));
insert(_el$3, createComponent(PageScript, {}), _el$10, _co$2);
insert(_el$3, createComponent(LiveReload, {}), _el$12, _co$3);
return _el$3;
})());
Broken, because getNextElement() always expects the first argument template2 not to be undefined… but it’s generated that way for _el$3.
function getNextElement(template2) {
let node, key;
if (!sharedConfig.context || !(node = sharedConfig.registry.get(key = getHydrationKey()))) {
return template2.cloneNode(true); // <== error happens in this line, template2 is undefined, see below
}
if (sharedConfig.completed)
sharedConfig.completed.add(node);
sharedConfig.registry.delete(key);
return node;
}
I mean, this is more or less the most basic SSR example you can make… idk, what I’m doing wrong here since months but I’m just lost and the generated code looks wrongs, so I guess opening a bug ticket is justified.
Please advice what I did wrong. I was following all the guides, read the code of the examples, the code of solid-ssr and also solid-start and couldn’t find a cure for this.
@ryansolid Is it maybe, that one cannot rehydrate the whole document from <html> down but only form a specific DOM tree node such as inside of <body>?
Thanks and best!
Your Example Website or App
https://codesandbox.io/s/flamboyant-sanne-duh3u3?file=/pages/index.ssg.js:63544-63584
Steps to Reproduce the Bug or Issue
Just visit the StackBlitz and enjoy 😃
Expected behavior
Solid would generate code with doesn’t break itself on hydrate() and that there would be some very simple example maybe for engineers to just realize what they are doing wrong just in case.
Screenshots or Videos
No response
Platform
- All browsers, all OS
Additional context
solid-js@1.6.6
“esbuild”: “^0.15.15”,
“esbuild-plugin-solid”: “^0.4.2”,
I also tried different versions of Solid, but nothing helps.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 20 (7 by maintainers)
I did end up mocking hydrate so with 1.6.7 onward you don’t need to lazily import it to work.
Glad you are set now.
I just meant fixing user stuff on your side. I was doing everything in the browser in codesandbox I modified the variable values while it was running to swap the element, set the context, and populate _el$13 (which was the one screwed up by the script tag in the body) and everything was good.
Looking at the codesandbox again now that it is updated we are back at two copies of Solid. There is a
getNextElementandgetNextElement2which is why they aren’t seeing each other as one is being used in your hydrate call and the other injected by the compiler. Looking closer it looks like both a ESM and CJS version of solid web are getting included.Yeah it’s due to the import cjs style in the entry. I tried changing the runtime.ts to:
And move the script tag inside the body and it works but it is unable to treeshake solid-js/web if we do that.
What I should do is stub out the hydrate and render calls in the server build. Rollup is smart enough to see isServer is constant and dead code eliminate out the missing branches and imports but it looks like ESBuild is not and tries to pull hydrate in even when it doesn’t exist on the server if we don’t do the dynamic import.
Ok looking at this I’ve found a few things.
context.pageClientScriptPathis undefined … context is undefined which causes a can’t accesspageClientScriptPathof undefined error. I’m gathering this is intended to be server only but it gets hydrated. Now arguably I can fix this with Solid to only do whats in the body. I hadn’t considered this case because a script tag isn’t allowed to be outside of the body or head. Which brings me to:When I correct all three of these things the page hydrates.
I see. Bundling the client twice is definitely an issue for Solid because reactivity works off a singleton as does the server context we use during SSR. I was hoping that was all it is. Now we are more likely dealing with a hydration mismatch. The ids start from the mount point. So as long as the server and client mount from the same element and render the same stuff we should be a match.
It does but it needs to start the counter and have it match up. It skips the head etc… but we need to be able to match ids and the id slotting happens at compiler time. I could be a bit smarter with
htmlprobably which might simplify some things with people starting from a mismatch to begin with. But depending on how crazy people get with breaking apart their templates I imagine there would still be edge cases. I can only detect the content of a given string at compiler time and not how it will be inserted.Ok looking at the very first example there are 2 copies of Solid being pulled in which would mess everything up, completely botch hydration.
I’m gathering the same is happening in the new server build because I see solid code in dist, but your build script is also importing solid code from a different location. It needs to set it as external or put
renderToStringin the built part.That gets it rendering.
I haven’t had a chance to look at this yet, but some perhaps useful info in the meantime. Certain templates can’t be cloned anyway. Like ones that contain
<html>so compiled code looks mostly correct to me. It should never be cloned in its whole lifetime because it should only be hit once, during hydration and it should match on hydration IDs. This mismatch is the source of the problem, but I will need to look why it is off. ThatsharedRegistry.getshould never miss during hydration.