1

I'm trying to get a simple React application rendering on the server using vite-plugin-ssr, with a 3rd-party package. The 3rd-party package is react-live-runner, which creates an editable code field that the user can edit to see their changes in real-time.

Here are the steps I took:

  1. I checked-out the minimal react+vite+vite-plugin-ssr+redux example from the vite-plugin-ssr repo.
  2. Ran npm install && npm install react-live-runner, then npm run dev, and browsed to localhost:3000.
  3. At this point, the minimal example loads fine (and is rendered on the server). It only has a counter component in the UI.
  4. I added the import statement and the <LiveProvider>...</LiveProvider> code to the index.page.jsx file all at once, and saved the file.
  5. At this point, the UI rendered correctly, since it was done by a hot model reload - the server didn't render it.
  6. Next I reloaded the browser, which causes the code to be processed server-side.

This results in an exception during server-side rendering:

10:16:54 AM [vite-plugin-ssr] HTTP Request ERR /
/Users/antun/git/scratch/vite-plugin-ssr/examples/redux/node_modules/react-live-runner/dist/index.modern.js:1
... <snipped most of the file>...
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at internalCompileFunction (node:internal/vm:73:18)
    at wrapSafe (node:internal/modules/cjs/loader:1176:20)
    at Module._compile (node:internal/modules/cjs/loader:1218:27)
    at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
    at Module.load (node:internal/modules/cjs/loader:1117:32)
    at Module._load (node:internal/modules/cjs/loader:958:12)
    at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:169:29)
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

The server will throw the error as soon as I add the `import ... from 'react-live-runner' line in the index.page.jsx below:

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { LiveProvider, LiveEditor, LivePreview, LiveError } from 'react-live-runner'

export { Page }

function Page() {
  const count = useSelector((state) => state.value)
  const dispatch = useDispatch()

  const increment = () => dispatch({ type: 'counter/incremented' })
  const decrement = () => dispatch({ type: 'counter/decremented' })

  return (
    <>
      <h1>Redux-Controlled Counter</h1>
      Count: {count}. <button onClick={increment}>++</button> <button onClick={decrement}>--</button>
    </>
  )
}

Here is what the node_modules/react-live-runner/dist/index.modern.js looks like when I prettify it and strip out the code, to leave just the import/export statements:

import {
  useRunner as e
} from "react-runner";
export * from "react-runner";
import t, {
  useState as r,
  useEffect as n,
  Fragment as a,
  useCallback as l,
  useRef as o,
  useMemo as c,
  createContext as s,
  useContext as i
} from "react";
import p from "react-simple-code-editor";
import m, {
  Prism as u
} from "prism-react-renderer";

<SNIPPED CODE>

export {
  C as CodeBlock, E as CodeEditor, P as LiveContext, x as LiveEditor, N as LiveError, w as LivePreview, j as LiveProvider, f as defaultTheme, k as useLiveContext, h as useLiveRunner
};

The node_modules/react-runner/dist/index.modern.js file is similar - just ES6 style module imports/exports.

The node_modules/react-live-runner/package.json file contains:

  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "unpkg": "dist/index.umd.js",
  "exports": {
    "require": "./dist/index.js",
    "default": "./dist/index.modern.js"
  },
  "types": "dist/index.d.ts",

So far, I've tried:

Manually editing the node_modules/react-live-runner/package.json file and adding "type": "module". (I also had to do that to node_modules/react-runner/package.json to get past a similar second error.) That leads to other errors in node_modules/react-dom/cjs/react-dom-server-legacy.node.development.js, which don't have very useful messages.

antun
  • 2,038
  • 2
  • 22
  • 34

1 Answers1

1

I was able to come up with a somewhat hacky workaround:

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'

// Only import the react-live-runner on the client
let LiveProvider, LiveEditor, LivePreview, LiveError;
if (!import.meta.env.SSR) {
  const reactLiveRunner = await import('react-live-runner')
    .then(reactLiveRunnerModule => {
      LiveProvider = reactLiveRunnerModule.LiveProvider;
      LiveEditor = reactLiveRunnerModule.LiveEditor;
      LivePreview = reactLiveRunnerModule.LivePreview;
      LiveError = reactLiveRunnerModule.LiveError;
      });
}
  
export { Page }
    
function Page() {
  const count = useSelector((state) => state.value)
  const [initialRenderComplete, setInitialRenderComplete] = React.useState(false);
            
  const dispatch = useDispatch()
    
  const increment = () => dispatch({ type: 'counter/incremented' })
  const decrement = () => dispatch({ type: 'counter/decremented' })
  
  React.useEffect(() => {
    setInitialRenderComplete(true);
  }, []);
      
  let liveProvider = null;
  if (!import.meta.env.SSR) {
    liveProvider = (
          <LiveProvider code={'<h1>Hello, World!</h1>'}>
            <LiveEditor />
            <LivePreview />
            <LiveError />
          </LiveProvider>
    );
  }

  return (
    <>
      <h1>Redux-Controlled Counter</h1>
      Count: {count}. <button onClick={increment}>++</button> <button onClick={decrement}>--</button>
      <hr />
      { 
        initialRenderComplete ? liveProvider : null
      }
    </> 
  ) 
}

This does two things:

  1. It skips trying to import the problematic library on the server altogether. import.meta.env.SSR is vite-plugin-ssr's way of checking if the code is being executed on the server.
  2. It tracks whether the UI has already been evaluated once with the initialRenderComplete variable, and skips rendering the <LiveProvider>...</LiveProvider> DOM the first time around on the server and client.

#2 is necessary because if the client tries to render the <LiveProvider> block on initial load, it won't match the DOM that the server rendered, and you will get this error:

Hydration failed because the initial UI does not match what was rendered on the server

This approach is OK for my case, since the DOM that is being deferred doesn't really need to be rendered on the server - it's a dynamic code editor, so search engine optimization for that piece isn't essential.

antun
  • 2,038
  • 2
  • 22
  • 34