SSR with netlify functions not hydrating

Site: dyelax-ssr.netlify.app/.netlify/functions/default
Repo: GitHub - dyelax/netlify-functions-abs-import: Testing absolute imports with Netlify Lambda Functions

I’m trying to do super basic server-side rendering with netlify functions; however, it seems like the app isn’t being hydrated correctly. I have a single function, default, that renders the App component, which in turn just displays whether the window element exists or not. From what I understand, window should exist if the app has been hydrated, but when I run netlify dev and go to the function, it always displays “NOT hydrated”. (On the deployed site, linked above, it displays nothing. So I may have more than one issue going on.)

App.js

import React from 'react';

const App = props => {
  let content = typeof window === 'undefined' ? 'NOT hydrated' : 'hydrated';
  return <p>{content}</p>
}

export default App;

lambda-functions/default.js

import App from '../App.js'
import React from 'react';
import ReactDOMServer from 'react-dom/server';

exports.handler = (event, context) => {
  ReactDOMServer.renderToString(<App />)
  return {statusCode: 200, body: ReactDOMServer.renderToString(<App />)};
};

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.hydrate(<App />, document.getElementById('root'));

Hi, @dyelax, I replied to you about this in a support ticket as well. While this question is definitely welcome here our support team will almost never answer questions about custom code like this one.

Our scope of support only covers our services and doesn’t extend to troubleshooting custom code.

Other community members are welcomed and encouraged to reply here but our support team doesn’t have the time/people available to answer third-party code questions.

If you do get this working please do feel free to post and update here as I’m sure the solution would be helpful for others with similar issues.

If there are questions about the scope of our support, please reply here anytime (or to the support ticket if you prefer).

Hi @luke, thanks for the reply. The “custom code” in this question isn’t the focus — it’s just a short example to easily replicate the issue that I’m having with netlify functions (ie, when they are used to perform server side rendering, the app doesn’t hydrate as normal.). I figured this would help the support team understand my issue more clearly. It seemed to be helpful in similar questions I’ve asked in the past.

The core questions are about netlify features: 1. Why might app hydration not work when performing SSR from a netlify function? 2. Is this supported, or is there a fundimental reason netlify functions can’t perfrom SSR? Thanks

Hey @dyelax :wave:t2:

Netlify functions are tough to provide support for because they can do literally anything :laughing: they’re like Play-doh. So given that the Function is running, @luke can’t provide much support beyond that. Good news is there’s a team of folks here on the Community that love digging in deeper than the official support engineers can :slight_smile:

So for starters, I did hit https://dyelax-ssr.netlify.app/.netlify/functions/default from my CLI and got a 200 but no body content (seeing this on browser too). I think that may be because you’re not using the async tag on your function but still returning an object (the response object) instead of a callback function, which would be the correct way to do it if you had the async tag. So from:

exports.handler = (event, context) => {

to

exports.handler = async (event, context) => {

I’d also recommend making sure to set the response headers to the “text/html” MIME type


The other thing that’s important to talk through with React’s ReactDOMServer.renderToString() => ReactDOM.hydrate() workflow is what renders when and what changes. Unfortunately it’s really hard to find specifics on the technical details for that workflow, but the gist is this: when you render to string on the DOMServer, React builds and renders all of your components using the default state defined in useState(<this>), does not run effects, and prints the HTML as such for the tree, leaving behind certain extra nodes and hook-points for when React on the client hydrates.

When React on the client it goes ahead and runs effects and other async things, then/and attaches to all of the virtual DOM nodes in order to prep for user interaction. It does that really fast, but the one thing that Hydrate assumes is that it doesn’t need to change the HTML tree at all if you don’t change state in one of your effects. Meaning that if the tree was printed with “Not Hydrated” directly in the HTML and not as a result of an effect or state, then by contract of API, React on the client is not obligated to update that HTML. Not unless you change state - which always re-renders a component.


As a corollary / proof of concept for this exact workflow, I’ll point you to something I actually wrote for React-using Netlify’ers - react-ssg-netlify-forms (a turn-key integration for running Netlify forms on Gatsby or Next) but it operates on the same principle - it renders one thing when rendered by ReactDOMServer and then switches over once it’s running in the window.

From react-ssg-netlify-forms/index.js at master · jon-sully/react-ssg-netlify-forms · GitHub (the React component itself):

// Build determination
const [inNetlifyBuild, setInNetlifyBuild] = useState(true)
useEffect(() => {
  setInNetlifyBuild(false)
}, [])

and then down in the render itself:

  return (
    inNetlifyBuild
      ? <ServerComponent>
      : <ClientComponent>
  )

So since I know the component will always be rendered by DOMServer then hydrated, I use inNetlifyBuild=true as the default state (so <ServerComponent> is rendered) then when it’s hydrated client-side, the useEffect will run, change the state over, and <ClientComponent> will be rendered instead.

In the case of my library, the component swap is non-visual so nobody’s the wiser, but you can see a very brief flash of your server-rendered component or text if it’s a visual change. Usually that really is just a flash - maybe a few ms, but keep that in mind as necessary.

All that to say, I’d urge you to refactor

const App = props => {
  let content = typeof window === 'undefined' ? 'NOT hydrated' : 'hydrated';
  return <p>{content}</p>
}

to

const App = props => {
  [onClient, setOnClient] = useState(false)
  useEffect(() => {
    setOnClient(true)
  })

  return (
    onClient
      ? <p>Hydrated!</p>
      : <p>Not Hydrated :(</p>
  )
}

I hope all of that helps!


Jon