How to use useEffect on server-side?

Kasper Moskwiak
May 10, 2020 · 5 min read
Image for post
Image for post

What a bummer — useEffect does not work on the server-side 😱. When I finally set up SSR (server-side rendering) in my project I noticed that there is no data from external APIs in the source of my page. This seems legit — useEffect runs after component render and there would be another re-render after it fetches data. However, on the server-side, there is only one render cycle. No updates. No re-renders.

I came up with a simple solution:

Just render your application twice 🕶

And I did. This is how.

The plan

  1. Render the application on the server for the first time.
  2. Collect all effects.
  3. Execute effects and wait for the API responses.
  4. Render the application for the second time, this time with data.

A simple SSR project

Below is the simplest SSR example I managed to come up with. For now, focus on three files: Server.jsx, Client.jsx, App.jsx. It follows a common pattern in all SSR projects:

  • Server.jsx is an entry point on the server. It uses renderToNodeStream method to render the React app inside the Node.js server.
  • Client.jsx is an entry point in the browser. Notice that instead of render we use hydrate method.
  • App.js is a common file for both environments and it is the actual React app.

If you have any problems with this embed just visit this CodeSandbox project.

We start with this simple SSR example

All my data fetching is placed in the useEffect hook in App.jsx.

I encourage you to open the browser preview in a new window and lurk into the page source. The app indeed rendered on server-side — we see that the h1 was injected into the page source. However, there is no data from the API:

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/></head>
<body>
<div id="app">
<div data-reactroot="">
<h1>Adventure Time</h1>
</div>
</div>
</body>
<script src="/static/Client.js"></script>
</html>

Make it work ✨

First, we need a way to communicate between components and node.js server during rendering. We are going to use Context — it will allow us to pass effects to the server and send data back to components.

The final app can be found in this CodeSandbox. If you scroll down there is an embed of this sandbox.

Context

Create a new file Context.jsx

import React from "react";const Context = React.createContext({});export default Context;

To pass data using context, the application must be wrapped by Context.Provider. This will be done in Server.jsx and Client.jsx.

Server

In the request handler in Server.jsx we use renderToString to invoke first render of application. Application is wrapped by our Context.Provider with default data {requests: []}. This is where we will collect all effects.

After renderToString is finished we can wait for all effects to resolve. Since requests array is not needed anymore it can be deleted.

const contextValue = { requests: [] };renderToString(
<Context.Provider value={contextValue}>
<App />
</Context.Provider>
);
await Promise.all(contextValue.requests);
delete contextValue.requests;

In this moment contextValue is filled up with data from effects (I will explain in a moment how that happened). In the next step, we inject into HTML a <script> tag containing JSON of all data. We will use it later in Client.jsx.

Finally, we render our application for the second time using renderToNodeStream method. The application is wrapped by Context.Provider however this time with all data.

res.write(
`<script>window.initialData = ${JSON.stringify(contextValue)};</script>`
);
const htmlStream = renderToNodeStream(
<Context.Provider value={contextValue}>
<App />
</Context.Provider>
);

Client

As on the server-side, in the client-side inClient.jsx file we need to wrap our application in Context.Provider. The initial value of context is grabbed from the global variable window.initialData.

let value = {};
if (window && window.initialData) {
value = window.initialData;
}
hydrate(
<Context.Provider value={value}>
<App />
</Context.Provider>,
document.getElementById("app")
);

This takes us to App.jsx.

const App = () => {
const [people] = useServerEffect([], "people", () => {
return fetch("https://adventuretimeapi.herokuapp.com/people")
.then(res =>
res.json()
);
});
return (
<div>
<h1>Adventure Time</h1>
{people.map(person => (
<div>{person.fullname}</div>
))}
</div>
);
};

Notice, that we no longer use useState and useEffect hooks. Instead, we use a custom hook useServerEffect. Remember that fetch function preset inside of the effect function will be executed both on the server-side and the client-side. This is why we need to use something like isomorphic-fetch — an npm package that will allow us to use fetch in Node.js.

use(Server)Effect hook

This is the whole code of our custom hook. Let’s take a look…

import React, { useState, useContext } from "react";
import Context from "./Context";
const useServerEffect = (initial, key, effect) => {
const context = useContext(Context);
const [data] = useState(context[key] || initial);
if (context.requests) {
context.requests.push(
effect().then(data => (context[key] = data))
);
}
return [data];
};
export default useServerEffect;

It takes three arguments:

  • initial — this is an initial state, just like in useState .
  • key — after the effect resolves, data is saved in our context under this key.
  • effect — our effect function, it must return aPromise.

Notice that we use built-inuseContext to access our context.

Remember that on the server we render our application twice? This is what happens in our hook during these renders:

1️⃣During the first render, our context looks like this { requests: [] }. There is no data yet, only an empty request array. This is when we grab our effect and save it to context. Notice that when it resolves, it puts data in the context under the key.

2️⃣During the second render, the context is already filled up with data. There is also no request array. useState takes that data and sets up the state which later is returned to component.

This is the whole example. If you look into page source this time you will notice that all data fetched from API is present. Magic 🌈

Final application

If you liked this solution perhaps you will be interested in using useSSE — a custom react hook available as an npm package.

use-sse

This npm package makes it easier to use and has some additional features. Hope you will enjoy it :)

If you have any questions or issues write a comment here or go to GitHub page of this project.

The Startup

Medium's largest active publication, followed by +771K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store