I am a prolific user and lover of symbols and custom JS, so I run into all the problems when it comes to these two features.
The most problematic part is the Builder.isServer block in custom JS. I wanted to share my pain points.
First of all, the docs (Adding custom code in the Builder.io visual editor - Builder.io) don’t make it clear at all when the Builder.isServer block gets executed and what happens to the result of that execution.
I think it’s pretty natural to assume that something called isServer is being rendered during SSR on one’s own server. I’ve exhaustively tracked down every place where custom code is executed in the React SDK, and there are no codepaths where your code gets executed during SSR, because it’s always wrapped in a conditional that checks Builder.isBrowser.
So when does Builder.isServer execute? Only when you make a request to the content API, where Builder’s servers execute the code. The misleading name is the first cause of headaches: isServer should be renamed something like isContentApiRequest.
If Builder.isServer blocks only execute on content API requests, then how are the instances of state and context that your code sees populated? Since I assumed my code was running during SSR, I also assumed that it would render whatever I pass into the data and context props. But actually these props have no effect on state and context within an isServer block. They must be populated by Builder’s content API internally, and that behavior is undocumented.
Once it executes, what happens? Well, even though you have access to context, it doesn’t appear that you can modify context in an isServer block, which is an undocumented restriction.
isServer blocks can modify state, which gets serialized as part of the content API response. <BuilderComponent> in the React SDK and its equivalents in other SDKs appear to consume the server-determined state and interleave it with input from data and other sources. How does this all work? Undocumented.
There are no error messages at all if execution fails, so you can’t even debug your isServer blocks.
The whole async main() function in custom JS returns a promise, and the content API theoretically waits for that promise to finish. However, I’ve set 10 second timeouts and the replies come back instantly. So clearly there’s an undocumented timeout.
Finally, I’m sorry to say it, but DX in the Visual Editor around isServer is really poor and has sent me down many hours of wild goose chases:
- If you update the
isServerblock, it doesn’t fetch the new content/state from the API, then merge that state into what you see on the page. But theisBrowserblock does re-execute instantly, so you just assume thatisServerwill, too, especially if you think it’s running on localhost during SSR. To see the results ofisServer, you have to refresh, but this isn’t documented. - Not only do you have to refresh, you have to hit publish before you refresh. Even if you use
includeUnpublished: truein your request, it still only returns the results of executing whatever has been published. - It’s not immediately obvious but even if you get everything else right, you still won’t see state populated by
isServerblocks on preview pages where you’re not passing prefetched content to<BuilderComponent>. For example,isServeris broken in the Visual Editor for all examples except for the REST API example on this page: Integrating Symbols - Builder.io - Finally, the content state inspector is really buggy with
isServer. Even when everything else is set up right and you’ve published your changes, first of all you have to refresh the entire Visual Editor page (not just hit the refresh icon in the iframe) to see them. And even then, sometimes I getstate: {}in the inspector even whenconsole.loginside myBuilder.isBrowserblock shows that state was in fact correctly populated from theisServerblock.
If I could do a few cheap and easy things to improve the experience with isServer, I would:
- Include a comment in the
Builder.isServerblock of custom JS explaining that this code won’t actually run on your server, it will run during content API requests and only if it’s been published, and that you need to refresh the Visual Editor page after publishing to see the results (may need to do it a few times to bust the cache). Explain that preview pages that don’t explicitly populate<BuilderComponent>from a content API request will not reflect the results ofisServer(for example, the examples at Integrating Symbols - Builder.io). - Update the docs to reflect the above, especially Adding custom code in the Builder.io visual editor - Builder.io and possibly Builder.io Content API - Builder.io.
- Allow
isServerblocks to modify context. - Return execution errors as part of the content API response.
Some potentially more involved improvements include:
- Rename to
isContentApiRequestand keepisServeras a deprecated reference. - Explain in the docs what variables are available to an
isServerblock, exactly how they’re populated, and what effect they have on rendering/state/whatever. (Explain inputs and outputs.) - Explain the above, but this time for symbols and slots. For instance, how does an
isServerblock for a symbol execute when that symbol is part of a page and the page has been requested from the content API? Does the symbol’s content inputs populate state on the server? Etc. - Add some kind of UI to the Visual Editor that shows when there’s been an error executing an
isServerblock. - Fix the inspector!