DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 14: Server Side Rendering

Server Side Rendering

Introduction

Server-Side Rendering (SSR) is a technique that enables a React application to be rendered on the server rather than in the client’s browser. This approach offers several benefits over traditional client-side rendering, including improved performance, better SEO, and enhanced user experience. In this chapter, we’ll explore how SSR works in React and how to implement it in your applications.

Understanding Server Side Rendering

What is Server-Side Rendering?

Server-Side Rendering refers to the process of converting React components into HTML on the server before sending them to the client’s browser. With traditional client-side rendering, the browser receives minimal HTML and a bundle of JavaScript that needs to be executed before users can see the content. In contrast, SSR delivers fully rendered HTML to the browser, allowing users to see content immediately while JavaScript is still loading.

Client-Side Rendering vs. Server-Side Rendering

Client-Side RenderingServer-Side Rendering
Initial rendering happens in the browserInitial rendering happens on the server
Slower initial loadFaster initial content display
Poorer SEO performanceBetter SEO performance
Lower server loadHigher server load
Single-page application experienceCan still function as an SPA after initial load

Why Use Server-Side Rendering?

  1. Performance: Faster initial page load and Time to First Contentful Paint (FCP)
  2. SEO: Search engines can crawl the fully rendered content
  3. Social Media Sharing: Proper metadata for sharing on platforms like Facebook, Twitter, etc.
  4. Accessibility: Content is available even if JavaScript fails to load or execute
  5. User Experience: Users see content immediately rather than a loading spinner

Server-Side Rendering Frameworks for React

While you can implement SSR from scratch, several frameworks simplify the process:

Next.js

Next.js is the most popular React framework for SSR. It provides a powerful yet easy-to-use approach to building React applications with server-side rendering.

Key features:

  • Automatic server rendering
  • Static site generation (SSG) option
  • API routes
  • File-based routing
  • CSS/Sass support with built-in styling options
  • Code splitting
  • Hot module replacement during development

Remix

Remix is a newer full-stack React framework focusing on web standards and modern UX patterns.

Key features:

  • Nested routes
  • Loaders and actions for data handling
  • Error boundaries at the route level
  • Progressive enhancement
  • Optimistic UI updates

Gatsby

Gatsby primarily focuses on static site generation but also offers SSR capabilities.

Key features:

  • Extensive plugin ecosystem
  • GraphQL data layer
  • Image optimization
  • Performance optimizations built-in

Implementing SSR with Next.js

Since Next.js is the most widely adopted SSR solution for React, we’ll focus on implementing SSR using this framework.

Setting Up a Next.js Project

# Create a new Next.js project
npx create-next-app@latest my-ssr-app
cd my-ssr-app

# Start the development server
npm run dev

Creating Server-Rendered Pages

In Next.js, pages are automatically server-rendered by default. Create a page by adding a React component file to the pages directory:

// pages/index.js
export default function HomePage() {
  return (
    <div>
      <h1>Hello from the server!</h1>
      <p>This page was rendered on the server.</p>
    </div>
  );
}

Data Fetching Methods

Next.js provides several data fetching methods for fetching data on the server:

1. getServerSideProps

Use getServerSideProps when you need to fetch data on every request:

// pages/products.js
export default function Products({ products }) {
  return (
    <div>
      <h1>Products</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>{product.name} - ${product.price}</li>
        ))}
      </ul>
    </div>
  );
}

// This runs on the server for every request
export async function getServerSideProps() {
  // Fetch data from an API
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
 
  // Pass data to the page via props
  return {
    props: {
      products,
    },
  };
}

2. getStaticProps

Use getStaticProps when data can be fetched at build time:

// pages/blog.js
export default function Blog({ posts }) {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

// This runs at build time
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
 
  return {
    props: {
      posts,
    },
    // Re-generate at most once per hour
    revalidate: 3600,
  };
}

3. getStaticPaths

Use getStaticPaths with getStaticProps for dynamic routes:

// pages/posts/[id].js
export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// Specify which paths to pre-render
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
 
  // Get the paths we want to pre-render
  const paths = posts.map((post) => ({
    params: { id: post.id.toString() },
  }));
 
  return {
    paths,
    fallback: 'blocking', // or true or false
  };
}

// Fetch data for each post
export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.id}`);
  const post = await res.json();
 
  return {
    props: {
      post,
    },
    revalidate: 3600,
  };
}

Hydration

After the server sends HTML to the client, React needs to “hydrate” the application, which means attaching event listeners and making the page interactive. Next.js handles this automatically.

The process works as follows:

  1. Server renders React components to HTML
  2. HTML is sent to the client with a reference to the JavaScript bundle
  3. Client downloads and executes the JavaScript
  4. React “hydrates” the HTML, preserving the server-rendered content while adding interactivity

Common SSR Challenges and Solutions

1. Browser-Specific APIs

Code that uses browser-specific APIs (like window or document) will fail during server rendering.

Solution: Use conditional checks or useEffect

import { useEffect, useState } from 'react';

function WindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });
 
  useEffect(() => {
    // This code only runs on the client
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
   
    window.addEventListener('resize', handleResize);
    handleResize(); // Call once to set initial size
   
    return () => window.removeEventListener('resize', handleResize);
  }, []);
 
  return (
    <div>
      {windowSize.width ? (
        <p>Window size: {windowSize.width}px x {windowSize.height}px</p>
      ) : (
        <p>Loading window size...</p>
      )}
    </div>
  );
}

2. State Management with SSR

When using Redux or other state management libraries with SSR, you need to:

  • Create a new store instance for each request
  • Initialize the store with data from the server
  • Serialize the store state and send it to the client

Example with Next.js and Redux:

// lib/store.js
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

export function initializeStore(preloadedState = {}) {
  return configureStore({
    reducer: rootReducer,
    preloadedState,
  });
}
// pages/_app.js
import { Provider } from 'react-redux';
import { initializeStore } from '../lib/store';
import { useStore } from '../lib/useStore';

export default function MyApp({ Component, pageProps }) {
  const store = useStore(pageProps.initialReduxState);
 
  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}

3. CSS in SSR

Styling can be tricky with SSR. Solutions include:

  • CSS Modules: Works well with Next.js out of the box
  • Styled Components: Requires special setup for SSR
  • Emotion: Similar to Styled Components
  • Tailwind CSS: Works well with SSR

Example with styled-components in Next.js:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
   
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });
     
      const initialProps = await Document.getInitialProps(ctx);
     
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }
 
  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Universal (Isomorphic) JavaScript

Universal or Isomorphic JavaScript refers to code that can run both on the server and in the browser. This is a key concept in SSR applications.

Writing Universal Code

When writing universal code:

  1. Avoid direct access to browser or Node.js specific APIs
  2. Use environment checks when necessary
  3. Use abstractions that work in both environments
// An isomorphic fetch function
import nodeFetch from 'node-fetch';

const isomorphicFetch = async (url, options) => {
  if (typeof window !== 'undefined') {
    // Browser environment
    return window.fetch(url, options);
  } else {
    // Node.js environment
    return nodeFetch(url, options);
  }
};

export default isomorphicFetch;

Performance Optimization for SSR

1. Code Splitting

Even with SSR, it’s important to minimize JavaScript bundle size:

// Using dynamic imports in Next.js
import dynamic from 'next/dynamic';

// Component is loaded dynamically only when needed
const DynamicChart = dynamic(() => import('../components/Chart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false, // Disable SSR for this component
});

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <DynamicChart />
    </div>
  );
}

2. Caching

Implement caching strategies to reduce server load:

  • Use CDNs for static assets
  • Implement cache headers
  • Consider Redis or Memcached for data caching
  • Use Incremental Static Regeneration (ISR) in Next.js

3. Streaming SSR

Newer versions of React and frameworks like Next.js 13+ support streaming SSR, which allows sending HTML in chunks as it’s generated:

// Next.js 13+ app directory approach
// app/page.js
import { Suspense } from 'react';
import SlowComponent from '../components/SlowComponent';

export default function Page() {
  return (
    <div>
      <h1>Welcome to my app</h1>
     
      {/* This content will be shown immediately */}
      <p>This content is available right away</p>
     
      {/* SlowComponent will stream in when ready */}
      <Suspense fallback={<p>Loading slow component...</p>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

SSR in Production

Deployment Considerations

When deploying SSR applications:

  1. Node.js Server: You need a Node.js environment to run the server-side code
  2. Memory Requirements: SSR uses more memory than static sites
  3. Scaling: Consider horizontal scaling for high-traffic applications
  4. Containerization: Docker can be useful for consistent deployments

Deployment Platforms

Several platforms make deploying SSR React applications easy:

  • Vercel: Optimized for Next.js deployments
  • Netlify: Supports SSR via serverless functions
  • AWS Amplify: Managed hosting with SSR support
  • DigitalOcean App Platform: Simple deployment for SSR apps
  • Heroku: Traditional platform with good Node.js support
  • Self-hosted: Using PM2, Nginx, etc.

When Not to Use SSR

While SSR has many benefits, it’s not always the best choice:

  • Admin Dashboards: Internal tools often don’t need SEO
  • Simple Applications: The added complexity may not be worth it
  • API-Only Use Cases: When your app is consumed only via API
  • Limited Server Resources: SSR increases server load

In these cases, consider client-side rendering or static site generation instead.

Hybrid Approaches

Modern frameworks support hybrid approaches that combine the benefits of multiple rendering strategies:

  1. Static Site Generation (SSG): Pre-render pages at build time
  2. Incremental Static Regeneration (ISR): Update static pages in the background
  3. Client-Side Rendering (CSR): Render non-critical pages on the client
  4. Server-Side Rendering (SSR): Use for pages that need fresh data

Next.js allows mixing these approaches in a single application:

// Static page with ISR
// pages/products/index.js
export async function getStaticProps() {
  // ...fetch data
  return {
    props: { products },
    revalidate: 60, // Update at most every minute
  };
}

// Server-rendered page
// pages/profile.js
export async function getServerSideProps({ req }) {
  // Get user from session cookie
  const user = getUserFromCookie(req);
  return { props: { user } };
}

// Client-rendered component
// components/RealTimeData.js
function RealTimeData() {
  const [data, setData] = useState(null);
 
  useEffect(() => {
    // Fetch on the client only
    fetchRealTimeData().then(setData);
  }, []);
 
  return data ? <div>{data}</div> : <div>Loading...</div>;
}

Summary

Server-Side Rendering offers significant benefits for React applications, particularly in terms of performance, SEO, and user experience. Modern frameworks like Next.js make implementing SSR straightforward, providing powerful features that help you build robust server-rendered applications.

As you implement SSR in your projects, remember to consider:

  • The right data fetching strategy for each page
  • How to handle browser-specific code
  • Performance optimization techniques
  • Whether a hybrid approach might be best

In the next chapter, we’ll explore the React Context API, which provides a way to share data across components without prop drilling.

Additional Resources

Scroll to Top