Skip to content
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

Enable Simple SSR #841

Merged
merged 1 commit into from
May 23, 2020
Merged

Enable Simple SSR #841

merged 1 commit into from
May 23, 2020

Conversation

gchallen
Copy link
Contributor

@gchallen gchallen commented May 21, 2020

What's in this PR?

react-ace is almost SSR ready. The main render function merely inserts an empty div, which is then populated by ace in componentDidMount. Because componentDidMount is not fired during SSR, SSR should result in an empty div. Ideally we would start with the content that would be present after the initial call to editor.resize(), but an empty div is better than failing.

List the changes you made and your reasons for them.

Currently react-ace fails during SSR because require("ace-builds") fails. ace-builds expects window to exist, but only to attach a global ace object to. react-ace doesn't actually use that global ace instance, rather it uses the handle to ace returned by require. And, again,
because all of the work is done in componentDidMount, just not failing during SSR doesn't require mocking up much of any kind of realistic window object like jsdom might try to do. It turns out that a simple empty object suffices.

Essentially here's what currently happens:

// Blows up during SSR since window is not defined
if ((window as any).ace) {

So let's add an SSR check:

if (typeof window === "undefined") {
  // Now this blows up because it wants to set window.ace to the ace instance
  ace = require("ace-builds");

So instead we can simply do this:

if (typeof window === "undefined") {
  global.window = {}
  ace = require("ace-builds");
  delete global.window;

And all is right with the world.

While this may seem like a trivial change, currently react-ace cannot be used out of the box with tools like Gatsby that do SSR for production builds. Workarounds exist using libraries like @loadable/component, but why bother with another dependency when the solution is fairly simple?

FWIW I have a minimal working Gatsby example that I can also add if its helpful. I'm also working on more complete SSR support using jsdom, but that will represent a more complex PR so I thought I'd file the simple one first.

react-ace is almost SSR ready. The main render function merely inserts
an empty div, which is then populated by ace in componentDidMount.
Because componentDidMount is not fired during SSR, SSR should result in
an empty div. Ideally we would start with the content that would be
present after the initial call to editor.resize(), but an empty div is
better than failing.

Currently react-ace fails during SSR because require("ace-builds")
fails. ace-builds expects window to exist, but only to attach a global
ace object to. react-ace doesn't actually use that global ace instance,
rather it uses the handle to ace returned by require. And, again,
because all of the work is done in componentDidMount, just not failing
during SSR doesn't require mocking up much of any kind of realistic
window object like jsdom might try to do. It turns out that a simple
empty object suffices.

Essentially here's what currently happens:

// Blows up during SSR since window is not defined
if ((window as any).ace) {

So let's add an SSR check:

if (typeof window === "undefined") {
  // Now this blows up because it wants to set window.ace to the ace
  // instance
  ace = require("ace-builds");

So instead we can simply do this:

if (typeof window === "undefined") {
  global.window = {}
  ace = require("ace-builds");
  delete global.window;

And all is right with the world.

While this may seem like a trivial change, currently react-ace cannot be
used out of the box with tools like Gatsby that do SSR for production
builds. Workarounds exist using libraries like "@loadable/component",
but why bother with another dependency when the solution is fairly
simple?
@securingsincity
Copy link
Owner

@gchallen this is awesome! and something that has been constantly asked for that I haven't had time for (haven't had a lot of time for much on this project lately) I'll merge tonight and prep a release along with some other updates - deps updates etc

@gchallen
Copy link
Contributor Author

gchallen commented May 21, 2020

Fantastic! Great to hear. I'm a heavy user of this library for my course websites so I'm happy to see it continuing to be maintained.

FWIW the "right" thing to do for SSR would be to try and output the HTML that Ace would render after the initial load, rather than an empty div. I took a stab at this this morning using jsdom and it was distinctly unfun. Using an approach similar to what you do during testing I was able, however, to get Ace to output some html.

However, the problem is that Ace internally uses a bunch of layout hacks that depend on being able to measure the width of components inserted into the DOM—for example, they measure character width by creating a hidden div, adding some text, and then measuring the resulting width. Understandably, jsdom doesn't support component widths since that would require doing layout calculations, and pretty soon you have a complete browser. I was able to work around this in a few places by essentially manually manipulating the jsdom objects and their prototypes, but what was emerging seemed quite brittle and unlikely to work more generally.

The right strategy may be to try and put together a pseudo-Ace by setting up some divs that mimic how Ace renders the editor. There are a few places where they do manual width or padding calculations, but you might be able to avoid these by using more modern HTML layout like flexbox. (For example, when I gave up I was stuck at getting the left gutter placed properly, since this is done through a manual left positioning of the scroller component that contains the actual editor text.) The goal would be able to reuse their CSS and the theme CSS to the greatest degree possible. (Although now that I think about it, you might still need to run JSDOM so that you can get Ace to run its syntax highlighter, even if the content ends up being plucked out and inserted somewhere else.)

Why do this? Mainly because I have a bunch of pages that contain a bunch of Ace editors and the loading overhead starts to become a problem. So I'd like to either lazy-load them somehow and get them off of the render path, or only load the actual editor after someone clicks in the example and only show the code until that point.

Anyway, if you have any interest in this or ideas about how to proceed let me know. I'd be happy to open an issue to discuss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants