Darling, I converted our perfectly fine SPA application into SSR: Part 2
Tags: webdev, casestudy, typescript, nextjs
Published: 2024-05-14 09:59:54

## Introduction

If you're just tuning in, the [first chapter](https://dev.to/findlabs/flowdiver-the-road-to-ssr-part-1-448k) detailed our approach to creating the SPA (Single Page Application) version of [Flowdiver](https://www.flowdiver.io/). We explained the reasoning behind our decisions and the challenges we encountered.👍

In this chapter, we'll discuss our experiences in migrating the SPA to a Next.js framework, employing an SSR (Server-Side Rendering) strategy to tackle some of the ongoing "problems."

## The Lab

### Preparation

To ensure that our decision was the right one, we took some time to evaluate other existing solutions on the market.

Next.js stood out due to its large community, the abundance of examples, and the availability of templates. However, the "recent" shift from a pages router to an app router, along with the introduction of client and server components, and the overall feeling that Next.js might be too cumbersome for our needs, sent us on a quest for new horizons.

### Qwik

We just wrapped up our [FindLabs corporate website](https://www.findlabs.io/) using Qwik and thought we could utilize our newly acquired experience.

#### Pros

Qwik was incredibly fast and straightforward to set up for SEO and achieving good web metrics. The clear separation between client and server-side code, utilizing `useVisibleTask$()` was a breeze to manage and didn't require complex mental gymnastics to understand the migration process.

#### Cons

The SPA version heavily utilizes [Styled Components](https://styled-components.com/), and although it's feasible to use the `styled-vanilla-extract` library and migrate the code with minimal changes, some parts would still need refactoring since CSS is pre-built during compilation. We've previously used the `useStylesScoped$` function while building a corporate website, but it often felt more like a hack than a solid solution.

Another significant challenge was the [render props](https://www.patterns.dev/react/render-props-pattern/) pattern. We heavily rely on this for our table component to ensure a high level of customization and abstraction. However, Qwik's architecture is fundamentally different—the framework aims to fragment the code into the smallest possible chunks and load them in parallel. This means it attempts to [serialize your render functions](https://qwik.dev/tutorial/props/closures/), which becomes a mess real fast.

Additionally, the contexts that Flowdiver heavily depends on would be unworkable and would require a complete redesign into a new composition. While these changes are manageable, we wanted to minimize disruption as much as possible.

Considering all these pros and cons, we decided to put this on the bench for a better use case 🙏.

### Waku

While researching server-side components, we discovered **[Waku](https://waku.gg/)**—a minimal React framework that facilitates the use of SSR (Server-Side Rendering) and SSG (Static Site Generation) approaches effortlessly 👍.

#### Pros

Waku struck the perfect balance we were looking for—a compact footprint on both client and server sides, along with a clear division of domains.

The [front page](https://waku.gg/) did an excellent job of clearly and efficiently explaining the concepts of client-/server-side components: data fetching, SEO, routing—all the puzzle pieces began to fall into place.

We bootstrapped an app and attempted a minimal setup migration. The community and the author on Discord were incredibly responsive, offering valuable tips and explanations on various concepts and strategies.

Sounds like a dream, right? Well, yeah...

#### Cons

The unstyled concept was functioning flawlessly, but we had to abandon `styled-components` almost immediately. The documentation covered CSS modules well, but lacked information on styled components and similar approaches. At that point, we realized that sticking to a well-tried solution might backfire... 🙈

Similar to `Qwik`, the use of contexts in Waku also appeared to be challenging and required significant refactoring to manage props drilling effectively.

Moreover, as we were experimenting with it, new features were continuously being released, making the framework feel somewhat unstable and experimental as well.

### What’s Next?

In the end, we found ourselves back where we started, but with newfound knowledge and confidence in our choice.

#### **Pros**

Next.js stands out as the most seasoned of all React-based full-stack frameworks. With dozens of examples and templates available, and hundreds of resolved issues on GitHub backed by multiple contributors, there's reassurance that it won’t just disappear over the weekend because the original author decided to move on from his project.

It’s also a match made in heaven for Vercel deployment. True to its promises, some features (notably [PPR](https://nextjs.org/docs/app/api-reference/next-config-js/partial-prerendering)) are exclusively viable on Vercel due to the tight integration and finely tuned setup.

#### **Cons**

Server-side components still didn't make much sense with us at that time 😅

However, we decided it was time to shed our apprehensions and start tinkering!

## Take 1 - Heads-on!

We opted for the `app router` as it appeared more modern and future-proof. Moreover, it seemed like a logical next step from [react-router](https://reactrouter.com/en/main). It also supports `server components`, which were crucial for our application. Plus, the [layout fragments](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) really seemed promising!

With our mindset focused on minimal changes and limited time commitment, we set up the basics that would allow us to work with styled-components and render them on a blank page. Once this foundation was laid, we began to `migrate` (or more accurately, copy/paste 😅) our components into the Next.js-based repository.

*One component, 
Two components, done! 
Errors during compilation, 
None!*

Well, not exactly. We were soon overwhelmed with errors like:

```tsx
You're importing a component that needs createContext. It only works in a Client
Component but none of its parents are marked with "use client", so they're Server
Components by default.

  │ Learn more: https://nextjs.org/docs/getting-started/react-essentials
  │ ...

```

But we came prepared, haha! We just needed to tag some files with `use client`. So this one, that one... and that one too!

In the end, we marked almost EVERYTHING with `use client` and inadvertently exposed our Hasura token to the public domain. Not exactly what we had planned, but hey, at least it was functioning. A living and “breathing” system. Yes! 👍

## **Take 2 - We Require More Minerals**

The next step was to migrate the rest of the pages. So, we thought, why not transfer about a hundred files at once? What could possibly go wrong? Fortunately, with our root layout file flagged as `use client`, everything below it was also tagged as "dirty," and thus the compiler remained silent, untroubled by our actions.

However, trouble started brewing as soon as we began refactoring pages into server-side components...

Here’s the snag: `client components` can’t import `server components`. Yet, we desperately needed that `ThemeProvider` from `styled-components` to function correctly. Moreover, various other context providers needed to be positioned at the top, so they were accessible for data reading across the board.

We plunged into the refactoring, attempting to determine what should remain a `server component` and what needed to be client-side. Yet, a relentless cascade of - ***It only works in a Client Component*** - made it seem like we were running in circles, getting nowhere fast.

It was time to pull back, reassess, and develop a new "pipeline" for how we would transition our SPA into the promised land of SSR.

## **Take 3 - Slow, but Steady**

We stripped everything down to the bare bones and began piecing the pages back together, labeling only the absolutely essential elements as client components. Fortunately, most of these necessities were related to `styled-components` and nestled within the `styles.tsx` files.

Hooks, particularly `useState` and `useSearchParams`, were reliable indicators that a file belonged on the client side of the network boundary. However, some could seamlessly receive `searchParams` props directly from `page.tsx` without switching allegiances.

We tackled the migration one route at a time, and it all seemed to be going smoothly, until we encountered the more complex pages—like those for accounts or transactions:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ue7dw5l80wxbzxqy57x2.png)


These pages featured some static elements—like status and transfers at the top—and more dynamic components controlled by tabs. We considered collecting all the data and distributing it to child components via context, but that approach would only be effective in this specific scenario. The account page, in particular, demanded more diverse data, which would necessitate pulling from the server or refreshing the page with new search parameters.

This led us to the decision to utilize [parallel routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) for implementing the tabs. This strategy allowed us to establish clear distinctions in data fetching for each tab without the complication of tight coupling. As a result, the render tree remained neat and tidy, while all the logic was appropriately segregated within its own domains:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8kx310nvfegc1aqrqf5y.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7u5xbjilh0pzwel6ilra.png)


At this point `layout.tsx` in the sub-route `/tx/[id]` operates as a component, alongside `page.tsx` and all the parallel routes. We used `Suspense` to effectively display loading states, ensuring that user experience remains smooth and uninterrupted during data fetching. Importantly, we carefully manage security by not exposing our token when fetching data.

![Noice](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbDZndTlod2tzcmR3Y2ZzdGh2NjV5OGI4Zm53dmhsNGZhb2d5cno1aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/yJFeycRK2DB4c/giphy.gif)


## Houston, We Have a Problem

While most of the components and pages function as intended, some fall short...

Take our custom table component, for instance. In the SPA setup, displaying the loading state was straightforward—we simply retrieved it from the context. However, in the SSR framework, components like the table header and pagination controls need to be visible before the data is fetched. This necessitated some serious refactoring and clever component rearrangement.

Before, our context provider enveloped the table; now, this needs to be shifted into the `children` block because data fetching occurs server-side, across the network boundary. We refactored the code, injecting additional props into the table (since it could no longer access these from the context) and ended up with the following configuration:

While most of the components and pages works as intended, some don’t…

For example, our bespoke table component. In SPA implementation it was easy to show loading state - we can simply pull it from context. But in SSR our table  - at least header and pagination controls - should be visible before data is fetched. That was asking for a refactoring and some component juggling.

Previously, we had context provider wrapping up table, now it should be part of the `children` block, cause data fetching is on server side of network boundary. We refactored that code, passed extra props to the table (cause now it couldn’t read them from context) and end up with following:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tyxk2b2nicb5zc0174ys.png)



`AccountTransactionsProvider` is a server component that fetches the data and then passes it into `TableDataProvider` via props. And `TableDataProvider` is a client component that can properly work with contexts:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o8gjvv9hn5ijk8vufd47.png)



That was an `Aha!` moment in understanding how to compose server and client components. You can’t `import` server components from client boundary, BUT, you can render the result of their execution in your client components. You just need to pass them as `children` or render props!

So the following composition is perfectly fine:

```tsx
		<RootServerComponent>  {/* <--------- SERVER */}
			<ClientComponent renderProp={<SDFComponent/>}> {/* <--------- CLIENT */}
			
				{/* the following code will be passed as "children" */}
				<SmallServerComponent> {/* <--------- SERVER */}
					<InnerClientComponent> {/* <--------- CLIENT */}
						<ServerCounterComponent /> {/* <--------- SERVER */}
					</InnerClientComponent>
				</SmallServerComponent>

				{/* ... and then we can control where to put SDFComponent in layout */}
			</ClientComponent>
		</RootServerComponent>
```

If you prefer not to manage this composition at the root level and aim for modularity, you can achieve this by enhancing your `ClientComponent` to accept additional properties—for example, a `renderProp`. You can then trust that it will include the necessary representation, allowing you to construct your layout in any preferred manner.

Personally, I consider the `Render Props` pattern to be one of React's most powerful features.

## The Final Hurdle

The very last “challenge” involved transitioning from the `useSearchParams` hook provided by `react-router` to its counterpart in `next.js`. The twist with Next.js is that while it lets you read the search parameters, it doesn’t offer a direct setter. This requires a more hands-on approach involving a trio of hooks: `usePathname`, `useRouter`, and `useSearchParams`. To manage this, you have to construct your own setter. The process would typically look something like this:

```tsx
  const searchParams = useSearchParams();
  const router = useRouter();
  const pathname = usePathname();
  
  // later in code
  
  const params = new URLSearchParams(searchParams?.toString() || "");

  if (field) {
    params.set(field, fieldValue);
    params.set(field, fieldValue);
  } else {
	  params.delete(field)
  }

  router.replace(pathname + "?" + params.toString(), { scroll: false });
```

We moved this into a convenient hook, so we could reuse the code and update multiple parameters simultaneously—a necessity for managing the filters in our table. 

## Cherries on top 🍒🍰

Leveraging Next.js, a server-side solution, made setting OpenGraph metadata, titles, and descriptions for our pages simply a breeze. This final requirement was almost effortlessly fulfilled. However, we encountered a hiccup with `@vercel/og`, which refused to render fonts properly, even the `bold` variant of the default one.

After some troubleshooting, we discovered that Noto Sans, included in the package, [only supports a 400 weight](https://github.com/vercel/satori/issues/263#issuecomment-1500705357). Attempts to load custom fonts from the file system did not pan out as expected—the documentation and examples lacked clear guidance on implementation. Furthermore, deployment introduced peculiar bugs, even though the local solutions worked just fine.

Despite these challenges, experimenting with Satori (the library that lets you create SVG/PNG images by defining them with HTML) proved to be quite enjoyable. We would definitely consider using this package in future projects.

Additionally, caching was readily available right out of the box, allowing us to lessen the load on our servers and enhance the overall user experience.

## Postmortem

Aside from the initial confusion surrounding server components, the majority of the migration process unfolded smoothly.

The groundwork established during the SPA implementation proved to be robust and resilient, carrying us through the migration with only minimal adjustments needed.

Most of our headaches stemmed from `styled-components`. Despite the merits of this CSS-in-JS library, it doesn’t mesh as seamlessly with server components. Traditional CSS methodologies, such as BEM ([Block, Element, Modifier](https://getbem.com/)), CSS modules, or even utility-first frameworks like Tailwind, would have alleviated many of our difficulties. This lesson is a key takeaway that we plan to carry forward into our future projects.