Fred Johnson
Fred Johnson

Reputation: 2695

How to use React.hydrate when using vue?

Bit of an odd situation here - I have a website written in Vue and I want to demo a library I've written in react. I can avoid server side rendering (SSR) by wrapping ReactDOM.hydrate(ReactApp, document.getElementById('react'area')) but I don't want to do that. I want to render everything SSR, but I don't see how it's possible.

Here is my renderOnServer.js for vue:

process.env.VUE_ENV = 'server'

const fs = require('fs')
const path = require('path')

const filePath = './App/dist/server.js'
const code = fs.readFileSync(filePath, 'utf8')

const vue_renderer = require('vue-server-renderer').createBundleRenderer(code)

//prevent XSS attack when initialize state
var serialize = require('serialize-javascript')
var prerendering = require('aspnet-prerendering')

module.exports = prerendering.createServerRenderer(function (params) {
  return new Promise((resolve, reject) => {
      const context = {
        url: params.url,
        absoluteUrl: params.absoluteUrl,
        baseUrl: params.baseUrl,
        data: params.data,
        domainTasks: params.domainTasks,
        location: params.location,
        origin: params.origin,
        xss: serialize("</script><script>alert('Possible XSS vulnerability from user input!')</script>")
      }
      const serverVueAppHtml = vue_renderer.renderToString(context, (err, _html) => {
        if (err) { reject(err.message) }
        resolve({
          globals: {
            html: _html,
            __INITIAL_STATE__: context.state
          }
        })
      })
    })
});

So basically I'm configuring SSR above to read server.js:

import { app, router, store } from './app'

export default context => {
  return new Promise((resolve, reject) => {
    router.push(context.url)

    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents()

      if (!matchedComponents.length) {
        return reject(new Error({ code: 404 }))
      }

      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({ store, context })
        }
      }))
        .then(() => {
          context.state = store.state
          resolve(app)
        })
        .catch(reject)
    }, reject)
  })
}

and server.js above is just looking for the right vue component and rendering. I have a test react component:

import React from 'react'

export default class ReactApp extends React.Component {
    render() {
      return (
        <div>Hihi</div>
      )
    }
  }

and my vue component:

<template>
  <div id="page-container">
    <div id="page-content">
      <h3 class="doc-header">Demo</h3>

      <div id="react-page">

      </div>
    </div>
  </div>
</template>
<script>
<script>
  import ReactApp from './ReactApp.jsx'
  import ReactDOM from 'react-dom'

  export default {
      data() {
          return {
          }
      },
  }

  ReactDOM.hydrate(ReactApp, document.getElementById('#react-page'))
</script>

But obviously it won't work because I can't use document in SSR.

Upvotes: 25

Views: 1178

Answers (1)

Max Gram
Max Gram

Reputation: 695

Basically, the purpose of hydrate is to match react DOM rendered in browser to the one that came from the server and avoid extra render/refresh at load.

As it was pointed in the comments hydrate should be used on the client-side and React should be rendered on the server with renderToString.

For example, on the server it would look something like this:

const domRoot = (
  <Provider store={store}>
    <StaticRouter context={context}>
      <AppRoot />
    </StaticRouter>
  </Provider>
)
const domString = renderToString(domRoot)

res.status(200).send(domString)

On the client:

<script>
  const domRoot = document.getElementById('react-root')

  const renderApp = () => {
    hydrate(
      <Provider store={store}>
        <Router history={history}>
          <AppRoot />
        </Router>
      </Provider>,
      domRoot
    )
  }

  renderApp()
</script>

Technically, you could render React components server-side and even pass its state to global JS variable so it is picked up by client React and hydrated properly. However, you will have to make a fully-featured react rendering SSR support(webpack, babel at minimum) and then dealing with any npm modules that are using window inside (this will break server unless workarounded).

SO... unless it is something that you can't live without, it is easier, cheaper and faster to just render React demo in the browser on top of returned Vue DOM. If not, roll up your sleeves :) I made a repo some time ago with react SSR support, it might give some light on how much extra it will be necessary to handle.

To sum everything up, IMO the best in this case would be to go with simple ReactDOM.render in Vue component and avoid React SSR rendering:

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="my-compiled-react-bundle.js"></script>
<script>
  function init() {
    ReactDOM.render(ReactApp, document.getElementById('#react-page'))
  }
  init();  
</script>

Upvotes: 1

Related Questions