Slate: add server-side rendering

Created on 7 Jul 2016  Â·  12Comments  Â·  Source: ianstormtaylor/slate

This might already be possible, in which case all we need to do is port the examples to be rendered server-side. But if not, while porting there might need to be a few changes made.

feature ♥ help

Most helpful comment

None of this will fix the server side rendering.

Using an integer can work if we render only one page, but the server will probably render thousands of page using the same slate module (the ID can start at something like 10000), and each client will have a new slate instance for each page (the ID will always start at 0 when rendering the page).

I don't see a good solution for now, except using a custom integer ID starting at 0 for each Raw.deserialize; basically enforce that all documents start with index "0" (ID relative to the state).

All 12 comments

Pretty sure this is already possible, but if someone finds an issue with it let me know!

I don't want to port the examples right now because that'd mean we'd need to figure out some other hosting besides GitHub pages which is way too convenient.

Server-side rendering works fine, we are using with next.js for our styleguide: https://github.com/GitbookIO/styleguide/blob/7.0.0/pages/components/CodeEditor.js

The main issue is the key generation (currently using uid), because it caused the markup between server and client to be different:

next-dev.bundle.js:9913 Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
 (client) "48"><div data-key="3e9s" style="positio
 (server) "48"><div data-key="p9qw" style="positio

I'm not sure if the change in #396 will fix it.

@ianstormtaylor Do you have an idea on how we can fix this ?

Ah interesting, it sounds like that change to make it auto-incrementing will fix it then? Since it would auto-increment to the exact same numbers.

Otherwise, I think there would need to be a separate step before rendering where you deterministically regenerate the keys for the state. Let's see if we can avoid it thought with the newest fix!

@SamyPesse let me know if this is fixed or not now that we've changed to using the auto-incrementing IDs. Hopefully so 😄

Also I just realized since I changed it to depend on lodash.uniqueId() this might end up causing "drift" between the server and client in terms of the current ID index. If so, we can revert to just using a simple n++ instead.

None of this will fix the server side rendering.

Using an integer can work if we render only one page, but the server will probably render thousands of page using the same slate module (the ID can start at something like 10000), and each client will have a new slate instance for each page (the ID will always start at 0 when rendering the page).

I don't see a good solution for now, except using a custom integer ID starting at 0 for each Raw.deserialize; basically enforce that all documents start with index "0" (ID relative to the state).

Using incrementing integers shouldn't be something global. Can't we have a key-generator as an editor prop (with a simple incrementing integer generator as a default generator, but the key-store living inside the editor instance, e.g. this.lastKey and this.props.generateKey(this.lastKey) to assign a new key)?

@SamyPesse makes sense, totally forgot that.

@bkniffler the trouble is that the immutable models and <Editor> are (rightly) decoupled, so the editor isn't actually accessible in the places where the regeneration is needed. So I don't think that approach will work for us.

Would it be hard/inconvenient to just enforce that when server-side rendering you should send the state representation to the client-side with keys already intact? Such that when it loads there is no need to regenerate keys, and the rendering will match?

If you're storing the state as Raw {terse:true} in your database, that would involve looping the tree and adding a node.key = to each node, and then sending that pre-keyed JSON to the frontend instead? Something like:

let n = 0

function rekey(node) {
  node.key = '$' + n++
  if (node.nodes) node.nodes.forEach(rekey)
}

The $ prefix ensures no collisions what will be generated later on the client. I'm not super caught up on server-side rendering, so I'm probably missing something.

Actually sorry, just read @SamyPesse's proposal there at the end. If we did change the Raw serializer that could effectively do the same thing. We could just add an option that was regenerateKeys (or resetKeys to imply that there is a hard reset going on), which would rekey from 0 automatically when deserializing the first time.

And the collisions shouldn't matter as long as our insertNode key de-duping logic is right. I think right now it might need to be edited to continue retrying until a non-duplicate is achieved.

Okay, thanks for the explanation. I agree that @SamyPesse's proposition seems the most viable. I've had success with SSR and rehydration just doing this:

import React, { Component } from 'react';
import { Raw, Plain, setKeyGenerator } from 'slate';
import { batch } from '../utils/batch';

// Important part starting
let getCounter = () => {
  let count = 0;
  return () => `${count++}`;
};
const parseValue = (value, initialState) => {
  setKeyGenerator(getCounter());
  if (!value) return initialState ? parseValue(initialState) : Plain.deserialize('');
  return Raw.deserialize(value, { terse: true });
};
// Important part end

class SlateEditor extends Component {
    batch = batch(300);
    rawValue = null;

    constructor(props) {
      super();
      this.rawValue = getValue(props);
      this.value = parseValue(this.rawValue, props.initialState);
    }

    shouldComponentUpdate(props) {
      const newValue = props.value;
      const oldValue = this.props.value;
      if (newValue !== oldValue && this.rawValue !== newValue) {
        this.value = parseValue(newValue, undefined);
        return true;
      } return false;
    }

    changeValue = value => {
      this.value = value;
      this.forceUpdate();
      if (this.props.changeValue) {
        const rawValue = Raw.serialize(value, { terse: true });
        if (JSON.stringify(this.rawValue) !== JSON.stringify(rawValue)) {
          this.rawValue = rawValue;
          this.batch(() => this.props.changeValue(this.props, rawValue));
        }
      }
    }

    render() {
      return (
        <Editor {...this.props} value={this.value} onChange={this.changeValue} />
      );
    }
  };

I think exporting a regenerateKeys function besides setKeyGenerator would be a good start, additionally to have it as an option while deserializing, what do you think?

Cool, yeah something along those lines sounds good. Question for you, since you've got the server-side setup going:

If we were to only expose a single method called resetKeyGenerator() that simply restarted the internal counter at 0, and you called that before every Raw.deserialize on the server, would that solve the issue?

Just trying to see what is the least opinionated and API heavy way to do it!

Yeah, it would be totally sufficient to have it as a simple function, just as you describe.

import React, { Component } from 'react';
import { Raw, Plain, regenerateKeys } from 'slate';
import { batch } from '../utils/batch';

const parseValue = (value, initialState) => {
  regenerateKeys();
  if (!value) return initialState ? parseValue(initialState) : Plain.deserialize('');
  return Raw.deserialize(value, { terse: true });
};
Was this page helpful?
0 / 5 - 0 ratings

Related issues

chriserickson picture chriserickson  Â·  3Comments

ezakto picture ezakto  Â·  3Comments

ianstormtaylor picture ianstormtaylor  Â·  3Comments

ianstormtaylor picture ianstormtaylor  Â·  3Comments

vdms picture vdms  Â·  3Comments