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
isServer
block, it doesn’t fetch the new content/state from the API, then merge that state into what you see on the page. But theisBrowser
block does re-execute instantly, so you just assume thatisServer
will, 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: true
in 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
isServer
blocks on preview pages where you’re not passing prefetched content to<BuilderComponent>
. For example,isServer
is 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.log
inside myBuilder.isBrowser
block shows that state was in fact correctly populated from theisServer
block.
If I could do a few cheap and easy things to improve the experience with isServer
, I would:
- Include a comment in the
Builder.isServer
block 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
isServer
blocks to modify context. - Return execution errors as part of the content API response.
Some potentially more involved improvements include:
- Rename to
isContentApiRequest
and keepisServer
as a deprecated reference. - Explain in the docs what variables are available to an
isServer
block, 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
isServer
block 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
isServer
block. - Fix the inspector!