Skip to content

Render component only on the server, without "mounting" on the client #6985

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

Closed
BowlingX opened this issue Jun 7, 2016 · 19 comments
Closed

Comments

@BowlingX
Copy link

BowlingX commented Jun 7, 2016

Is it possible to have a "frozen" component, that is rendered only on server-side, without being processed on the client side? (without calling the render-cycle again).
Let's say I have a CMS, that statically generates HTML on the server (a legacy system), but I want to use react to render the output inside my component structure.

<Root>
  <ComponentA/>
  <CmsBlock id="staticMarkupId"/>
  <ComponentB/>
</Root>

To prevent the HTML to be exposed twice, I will provide the information only on the server side.
The HTML can be quite big, and I don't want to have them on the client site again.

Right now, I don't see a way to tell react, to prevent calling the render method on a component again.
What I did, is this ugly workaround (react complains about a checksum mismatch though):

/**
 * @flow
 */

import React, { Component, Element } from 'react';

type Props = {
  id:string,
  className?:string
}

type State = {
  markup:Object
}

export default class CmsBlock extends Component<void, Props, State> {

  props:Props;

  state:State;

  constructor(props:Props) {
    super(props);
    const { id } = props;
    const createMarkup = contentId => {
      return {
        __html: global.__CMS_CONTENT__ && global.__CMS_CONTENT__[contentId] ? global.__CMS_CONTENT__[contentId] :
          !global.__CMS_CONTENT__ && global.document ?
            // FIXME: WTF?
            global.document.querySelector(`[data-cms-id="${contentId}"]`).innerHTML :
            `CMS Block ID <strong>${contentId}</strong> does not exist.`
      };
    };

    this.state = {
      markup: createMarkup(id)
    };
  }

  shouldComponentUpdate() {
    // is not called on initial "client-mounting"
    return false;
  }

  render():Element {
    const { className, id } = this.props;
    return (
      <div data-cms-id={id} className={className} dangerouslySetInnerHTML={this.state.markup}/>
    );
  }
}

I used React 15.1.0.

@syranide
Copy link
Contributor

syranide commented Jun 7, 2016

That would certainly be possible but I wouldn't get my hopes up (it's rather niche) (EDIT: hmm, it's actually flawed due to checksum-mismatches). IMHO, the simplest workaround for what you're trying to achieve is probably just to render a simple GUID-string instead of the HTML. If you then string-replace this with the HTML before shipping it off to the client React will be none the wiser (it's technically legal too). As long as you don't change the value React won't replace it.

PS. But beware of checksum-mismatches, they will recreate the DOM and ruin the magic trick.

@BowlingX
Copy link
Author

BowlingX commented Jun 7, 2016

mhhh, the html should be rendered and visible when fetching the page already, and should also be there if the component mounts. React should just not try to render the component again on client side. I would maybe expect some hook to overwrite, like shouldComponentRenderAfterClientMount (too verbose but similar), where I could compare the server side props, content, checksums etc. and the one that are supplied on client side. And I could then decide what should happen next.

@syranide
Copy link
Contributor

syranide commented Jun 7, 2016

@BowlingX Theoretically there could be a dangerouslySetInnerHTMLOnServerOnly or w/e, but that could impossibly work if there's a checksum-mismatch and React being a client-side framework it doesn't really make a lot of sense to have a feature that only exists for a single niche server-side usage which also behaves in such an unconventional way.

Another possibility is to do what I said above, but keep the GUID as an ID on the node. Before mounting React client-side you (theoretically) find the node with the GUID, store it and unparent it from the DOM. Then let the component move it back inside itself upon render (by GUID), this would also survive checksum-mismatches. IMHO although a bit quirky it is probably the only in a sense proper solution to this problem and implemented right would also allow it to work transparently client-side as well.

@BowlingX
Copy link
Author

BowlingX commented Jun 8, 2016

I tried this approach, but the checksum is somehow still invalid (on the most outer component), I'm removing all staticly generated html nodes of the component before react boots and reinsert them on componentDidMount:

// during application setup, before react boots:
global.__CMS_CONTENT__ = Array.prototype.slice.call(
        global.document.querySelectorAll(`[data-cms-id]`)
      ).reduce((first, next) => {
        const node = next;
        const container = first[next.getAttribute('data-cms-id')] = [];
        while (node.firstChild) {
          container.push(node.firstChild);
          node.removeChild(node.firstChild);
        }
        return first;
      }, {});
/**
 * @flow
 */

import React, { Component } from 'react';

type Props = {
  id:string,
  className?:string
}

type State = {
  markup:Object
}

export default class CmsBlock extends Component<void, Props, State> {

  props:Props;

  state:State;

  constructor(props:Props) {
    super(props);
    const { id } = props;
    if(!global.document) {
      const createMarkup = contentId => {
        return {
          __html: global.__CMS_CONTENT__ && global.__CMS_CONTENT__[contentId] ? global.__CMS_CONTENT__[contentId] :
            `CMS Block ID <strong>${contentId}</strong> does not exist.`
        };
      };

      this.state = {
        markup: createMarkup(id)
      };
      return;
    }

    this.state = {
      markup: null
    }
  }

  parent:Element;

  componentDidMount() {
    global.__CMS_CONTENT__[this.props.id].forEach(childs => {
      this.parent.appendChild(childs);
    });
  }

  render() {
    const { className, id } = this.props;
    return (
      <div ref={parent => (this.parent = parent)}
           data-cms-id={id} className={className} dangerouslySetInnerHTML={this.state.markup}/>
    );
  }
}

@BowlingX
Copy link
Author

BowlingX commented Jun 8, 2016

The Error is:

warning.js:44 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) <div class="RootCon
 (server) <div class="RootCon

and the structure (simplified) looks like this:

<div class="RootContainer">
    <CmsBlock id="anId"/>
</div>

Before react would complain on the CmsBlock Element like this:

warning.js:44 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) data-reactid="251"><div></div></div><foo
 (server) data-reactid="251"></div><footer class="

@jimfb
Copy link
Contributor

jimfb commented Jun 8, 2016

To prevent the HTML to be exposed twice, I will provide the information only on the server side.
The HTML can be quite big, and I don't want to have them on the client site again.

Can you elaborate on exactly what your problem is? You are trying to save bandwidth by only sending the markup down to the client once?

Is it possible to have a "frozen" component, that is rendered only on server-side, without being processed on the client side? (without calling the render-cycle again).

In general, I think this is an antipattern. You should find a way for this to not be true. Anything you do to make this happen will be a hack at best (fragile and unsupported).

However, if I'm understanding your issue correctly, there are potentially other ways to solve this issue. For instance, you could store the SSR markup as a string. Use that string to immediately populate an empty DOM node (eg. a <div />) which was rendered by React.

@BowlingX
Copy link
Author

BowlingX commented Jun 8, 2016

@jimfb yeah, the idea is to save bandwidth and the markup should be correctly rendered without the need to have javascript enabled (for SEO and fallback purposes). I understand this looks like a hacky way but on the other hand, what about a lot of static generated markup that won't be touched, why should this be send twice to the client?

@jimfb
Copy link
Contributor

jimfb commented Jun 8, 2016

Yeah, I agree. When a page has lots of static content, sending down all the code to regenerate that static content seems like a crying shame. One mitigating factor is that you've already got the content in front of the user, so the background loading of data is somewhat less of a problem.

The generalized problem is: How can we avoid sending down static code/data that is never needed because it will never change after initial render. How could we load components/data only when absolutely necessary.

I think there could be some good research opportunities in this area, especially if/when we switch to an incremental renderer. However, I don't know of any short-term solutions that aren't horrible hacks/antipatterns.

@jimfb
Copy link
Contributor

jimfb commented Jun 10, 2016

I'm going to close this out, because I don't see anything actionable here. Feel free to continue the discussion on this thread, and we can re-open if we're able to come up with an action plan.

@jimfb jimfb closed this as completed Jun 10, 2016
@spudly
Copy link
Contributor

spudly commented Jul 15, 2016

So I just created a prototype for a solution to this problem. The basic idea is that you render the component twice on the server-side (once to get the html, once to get the checksum). For example, I have a RawHtml component that I want to render only on the server-side. I do not want to send the data twice (once as html, once as json).

The solution is this:

  • set shouldComponentUpdate on RawHtml to always return false.
  • On the server, render the component with it's props. (we'll call this htmlWithProps)
  • On the server, render the component once with no props (we'll call this htmlWithoutProps)
  • Parse the data-react-checksum attribute value from htmlWithoutProps and inject it into htmlWithProps - basically what we're doing here is lying to the client about the contents of the markup
  • send the following back to the client:
    • htmlWithProps
    • init logic that calls ReactDOM.render with no props for RawHtml
  • rejoice 🎉

When ReactDOM.render is called on the client. It will think (incorrectly, but that's good) that the DOM matches the props you gave it because the checksum matches a render without props. Because of this it will not remove the server-rendered RawHtml content from the page, and it will not give a checksum error. Also, because shouldComponentUpdate returns false, it never removes the content unless that component is fully unmounted.

So far I only see two problems with this:

  1. Performance overhead of double-rendering on the server-side.
  2. React team might break it because it relies on an implementation detail.

Anybody see any other problems with this?

@Amberlamps
Copy link

Amberlamps commented Aug 4, 2016

Why is this so complicated? And why hasn't this come up more often? Isn't it a general use case to not rerender a component on client-side when it comes rendered as markup from the server?

As I see it, there are three benefits of using server-side rendering:

  1. Every page is easily parsable by bots and therefore SEO works greatly.
  2. User see content instantly because client browser does not need to wait for anything.
  3. Client JS is not blocking other code or different process execution.

So, we still have 1. and 2., which is great, because they are the most important in my opinion.

@f0rr0
Copy link

f0rr0 commented Sep 9, 2016

I don't know if this pertains to the problem I am facing at the moment:

I am rendering some static content on the server, which is basically a lot of text. The React component expects the text to be on the this.props.content. I provide this as a prop to the component while rendering. But on the client, React again expects the content to be on this.props.content. How do I get that? There's no point in asking the server to send the content again because it is essentially already on the client's screen. If I put the content in a hidden div and try to populate the prop by deserializing that, I have to send a big file down on first render with both the server rendered component with the static content in it and the same content serialized in a hidden div. Is there something I am missing to work around this?

@jribeiro
Copy link

jribeiro commented Oct 10, 2016

@spudly Could we render htmlWithoutProps on a server bootstrap step and cache the data-react-checksum for subsequent renders thus avoiding the double rendering. Point 2 is concerning indeed. Any chance you can share your prototype?

Interesting that there doesn't seem to be much on this topic as, to me, some content is static (or really slowly changing) and it seems very wasteful to be sending these payloads twice on initial load.

Obviously the legacy CMS use cases (already mentioned) but also the case with Headless CMS and the ability to create an application on top of it without compromising the initial load performance.
Also I think there could be a case for allowing (opt-in) the checksum to be asynchronous. This would further reduce the bandwidth of a real time application where most likely you're rendering the data on the server, sending it on the initial state and then receiving it again when connecting to a websocket or so.

A lot of effort goes into server side rendering the applications and optimising code and to me this is crucial to the critical rendering path.

If shouldComponentUpdate was called on the initial render one could cancel or delay any rendering right? Obviously that would potentially break a lot of code but the component lifecycle sounds like the right place to me.

Still a bit new to React though so apologies if I'm missing something.

@Artur-A
Copy link

Artur-A commented Oct 20, 2016

Personally I do not understand why the problem of double loading is so underestimated.

There is a video where the Facebook team tries to implement a new engine to enhance rendering for a page, say from 100 ms to 60 ms. It looks like team spends time in an attempt to speed up a cheetah while there are two gigantic hippos behind it and everybody should wait for the hippos. Then we (developers) spend time to create a fix with a hack or spend our clients time to wait for a loading with duplicated data.

For example, there is a need from one of our customers to create a big static page with two dynamic elements. The UI itself is not simple, so it is great to build it with reusable React components. Also there is no need to make a JSON request to a server for texts and images after loading (as a workaround), the content and UI is unique and mostly static. Now the React page is rendered on the server side and is sent to the client, at the same time React client.js contains the copy of this huge data. This is a very common scenario and the really important problem.

Can someone please specify what exact issues the ReactJS team faces/foresees in attempt to resolve this problem?

A proposal for a client.js:

<Page>
        <HeaderComponent>
                 // The component is rendered as static HTML on the server side
                 // On the client side it is an empty placeholder
                <StaticHtmlPlaceholder id="idX"> 
                      // React engine goes to DOM and grabs the SSRed data,
                      // restores the static child component as it is 
                      // no need for props or meta data as it is pure static html at its final point 
                </StaticHtmlPlaceholder>  
               <DynamicComponent>
                       // ... dynamic part
                       <StaticHtmlPlaceholder id="idY"> 
                       </StaticHtmlPlaceholder>  
               <DynamicComponent>
        </HeaderComponent>
  <Page>

In the source code, which will be initially rendered by a server

<Page>
        <HeaderComponent>
                 <StaticHTML>
                         <h1>
                                 This is a static text inside some static control
                         </h1>
                         <StatelessFunctionalComponent>
                                 Just renders static html
                         </StatelessFunctionalComponent>
                 </StaticHTML>
       ...

In this way it is possible to mark static blocks that will never change. React can restore these points to Virtual DOM (or whatever) from DOM before the first rendering.

Even with stateful components and dynamic properties it is still possible to do:

   <ComplexStatefulComponent
            name={bind.Something.Dynamic}            
            description={
                <Static>
                        Looooooong static text.
                          // or mark static property as @const (or whatever is better) 
                </Static>}
            imgSrc={@const "Loooooong base64 image code"}          
      />

On the client side we again can restore the component as it would be the first place of truth.
After that even whole DOM on the page can be removed as Client.js will hold full source code.

Later it might be possible to write an algorithm which can automatically understand what part is static without manual marking.

[Server]
In this model a server is a manager who knows how rendered html is looking and rules the game. The server can eliminate parts from Client.js. And in more complex scenarios only the server knows which blocks to eliminate as some marked blocks could not be rendered to DOM.
This can be done, for example, with a client.js script dynamically created by a server for each request. Probably it can be made fast enough with versioning, caching and other enhancements.

React is an amazing library, really hope to see even initial/partial solution or good suggestions.

P.S. Should we rename or create a new issue with proper name? Doubled data is the real problem, not a component mounting.

@jwr
Copy link

jwr commented Dec 31, 2016

Hate to pitch in with a me-too, but I was pointed to this discussion after encountering the same problem in my ClojureScript/React app. I use server-side rendering, which works great, except for pages with lots of static content. I was hoping I could avoid transferring all the static content twice: after all, it is already in the DOM and will never change.

I hope this will eventually become a more visible problem once more people start encountering the same limitation.

@syranide
Copy link
Contributor

@jwr It is certainly possible to do this, but it means that the client has to perform different rendering logic from the server, i.e. the server component renders some initial HTML but the client component should then provide a value "don't change". That's not really a big problem, you can hack this yourself quite easily #6985 (comment).

@derKuba
Copy link

derKuba commented Feb 22, 2017

we have the same problem. we would like to have the possibility to just skip rendering a component which has already been rendered on server side.
@syranide could you make a example how a hack of this problem would look like?

@wilsonpage
Copy link

I have a similar problem. I have a large table (5000 rows) rendered on the server. i don't want to send the data to the client twice (in HTML and JSON form) I came up with this solution/hack.

Before rendering the table rows, the component first checks the DOM to see if a component exists with the same id (which if pre-rendered server-side will exist), if it does then it extracts the HTML and uses that directly via dangerouslySetInnerHTML.

  renderTable() {
    debug('render table');
    const id = 'table-body';
    const html = this.getExistingHtml(id);

    if (html) {
      return <tbody id={ id } dangerouslySetInnerHTML={{ __html: html }} />;
    }

    return <tbody id={ id }>{ this.renderItems() }</tbody>;
  }

  getExistingHtml(id) {
    if (typeof document === 'undefined') return;
    const node = document.getElementById(id);
    return node && node.innerHTML;
  }

  renderItems() {
    debug('render items');
    return this.props.items.map(({ name, url }) => {
      return (
        <tr>
          <td>
            <a href={ url }>
              <div>{ name }</div>
            </a>
          </td>
        </tr>
      );
    });
  }

  shouldComponentUpdate() {
    return false;
  }

I've also coupled this with shouldComponentUpdate to prevent any renders after the initial mount.

@gaearon
Copy link
Collaborator

gaearon commented Dec 21, 2020

We've made progress on this.

https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html

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

No branches or pull requests