DEV Community

Cover image for My approach to SSR and useEffect - discussion
Kasper Moskwiak
Kasper Moskwiak

Posted on • Updated on

My approach to SSR and useEffect - discussion

For the last few days, I was developing my personal website. I felt it needed some refreshment and as always it is a great occasion to play with something new. I've decided it will be written in React with SSR.

I've put all data fetching in useEffect hook - pretty standard approach. However, useEffect does not play very well with server-side rendering. I've managed to work this out by creating custom hook useSSE - "use server-side effect" and I've created an npm package from it.

I am very curious about your opinion. Here is the package on npm and GitHub repo:

GitHub logo kmoskwiak / useSSE

use Server-Side Effect ✨in React SSR app


And here is an example on CodeSandbox.

And this is how it works...

Instead of using useEffect for data fetching, I use useSSE. It looks like a combination of useState and useEffect. Here is an example:

const MyComponent = () => {
  const [data] = useSSE(
    {},
    "my_article",
    () => {
      return fetch('http://articles-api.example.com').then((res) => res.json());
    },
    []
  );

  return (
    <div>{data.title}</div>
  )
}
Enter fullscreen mode Exit fullscreen mode

useSSE takes 4 arguments:

  • an initial state (like in useState)
  • a unique key - a global store will be created, and data will be kept under this key,
  • effect function returning promise which resolves to data,
  • array of dependencies (like in useEffect)

The essence of this approach is to render application twice on server. During first render all effect functions used in useSSE hook will be registered and executed. Then server waits for all effects to finish and renders the application for the second time. However, this time all data will be available in global context. useSSE will take it from context and return in [data] variable.

This is how it looks on server-side. The code below shows a part of expressjs app where the request is handled.

app.use("/", async (req, res) => {
  // Create context
  // ServerDataContext is a context provider component
    const { ServerDataContext, resolveData } = createServerContext();

  // Render application for the first time
  // renderToString is part of react-dom/server
    renderToString(
        <ServerDataContext> 
            <App />
        </ServerDataContext>
    );

  // Wait for all effects to resolve
    const data = await resolveData();

  // My HTML is splited in 3 parts
    res.write(pagePart[0]);

  // This will put <script> tag with global variable containing all fetched data
  // This is necessary for the hydrate phase on client side
    res.write(data.toHtml());

    res.write(pagePart[1]);

  // Render application for the second time. 
  // This time take the html and stream it to browser
  // renderToNodeStream is part of react-dom/server
    const htmlStream = renderToNodeStream(
        <ServerDataContext>
            <App/>
        </ServerDataContext>
    );

    htmlStream.pipe(res, { end: false });
    htmlStream.on("end", () => {
        res.write(pagePart[2]);
        res.end();
    });
});
Enter fullscreen mode Exit fullscreen mode

On client-side application must be wrapped in provider as well. A custom context provider is prepared for this job. It will read data from global variable (it was injected by this part of code: res.write(data.toHtml())).

const BroswerDataContext = createBroswerContext();

hydrate(
    <BroswerDataContext>
        <App />
    </BroswerDataContext>,
    document.getElementById("app")
);
Enter fullscreen mode Exit fullscreen mode

That's it! What do you think about this approach? Is useSSE something you would use in your project?

Here are all resources:

Oldest comments (3)

Collapse
 
vengeurmasque78 profile image
TheVengeurMasque

Have you seen the demo released by Dan Abramov regarding streaming data onto the server ? codesandbox.io/s/festive-star-9hfq...
Do you think latest version of react can allow us to stream data from promise instead of waiting and rendering twice ?

Collapse
 
kmoskwiak profile image
Kasper Moskwiak

Thanks for the link, it looks very exiting. I'll try to make some examples with this new API combined with useSEE. I have to dive into this a little bit...

However my first thought is that useSEE will still need two renders. The new pipeToNodeWritable will just replace renderToNodeStream in second render.

Collapse
 
vengeurmasque78 profile image
TheVengeurMasque

From what I understand in Dan's abramov demo, the component wrapped within suspense is not rendered. If you inspect the page, you won't see any html.
React 18 does not have to wait for all promises to resolve.
When a promise resolve, it will then render the code server side and "inject it" on the client. That's the beauty of it. It solves the waterfall issue if I understood correctly.
Though Dan abramov said they have not implemented the data fetching mechanism.
Does useSEE fetch data twice (server + client) ? From what you wrote, you said that data will be available in the global context.
So I guess we have to check if data exist in the global context we fetch or we don't
Something like

const [data] = useSSE(
    {},
    "my_article",
    () => {
      if (!fetchedData) {
      return fetch('http://articles-api.example.com').then((res) => res.json());
      }
    },
    []
  );
Enter fullscreen mode Exit fullscreen mode