Builder.isServer block in custom JS is a mysterious black hole and needs some love

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 the isBrowser block does re-execute instantly, so you just assume that isServer will, too, especially if you think it’s running on localhost during SSR. To see the results of isServer, 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 get state: {} in the inspector even when console.log inside my Builder.isBrowser block shows that state was in fact correctly populated from the isServer 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 of isServer (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 keep isServer 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!

hey @ersin thank you for the detailed feedback! I have shared with our internal product team and we will keep you updated on any findings or changes that come from it!

It is our intended goal to have Builder.isServer run during server side rendering, but unfortunately there are a few things intrinsic to React that make this difficult. For example, you can’t do anything async during server side rendering with React. We have a number of SDKs for other frameworks in development that should be able to achieve this, and intend to fully support this with our own frontend Framework Qwik as well.

Really appreciate your research here as I think it will help a lot of other users who have had questions around Builder.isServer !

1 Like

Thanks, @TimG!

Also, a quick update to part of my original post:

I had created a timeout function that returned a promise like this:

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

My original point was that running something like await timeout(10000) within a Builder.isServer block doesn’t actually cause the content API request to pause. However, when I adapted my code to get a jerry-rigged error response, I realized the problem: setTimeout isn’t defined within the content API’s server execution environment.

So the issue in my particular case wasn’t actually that there’s a short timeout, but my diagnosis just goes to show you how important it is to include some kind of error reporting.

For anyone who’s curious, here’s how I got an error back:

if (Builder.isServer) {
  try {
     ...
  } catch (error) {
    state.error = error.toString();
  }
}

Then inspect state on the response. Works in a pinch.

1 Like