SSR with Remix using TanStack Query

Server-Side Rendering (SSR) is a technique used in web development where the HTML is rendered on the server instead of the client. This results in faster initial page loads and better SEO performance. In this article, we will explore how to implement SSR using Remix and TanStack Query.

Remix is a full-stack React framework focused on SSR, which allows developers to build both the frontend and backend within the same application. TanStack Query (formerly known as React Query) is a powerful tool for managing server-state in React applications, offering features like caching, synchronization, and more.

By the end of this article, you will have a comprehensive understanding of how to set up and use SSR with Remix and TanStack Query, along with best practices and real-world examples.

Understanding Server-Side Rendering and Its Benefits

Server-side rendering (SSR) involves rendering web pages on the server rather than in the browser. This approach contrasts with client-side rendering (CSR), where the browser renders the web page using JavaScript. Here are some notable benefits of SSR:

Improved SEO

Since the HTML content is fully rendered on the server before being sent to the client, search engines can easily crawl and index the content, leading to better search engine optimization (SEO).

Faster Initial Page Load Times

SSR delivers fully rendered HTML to the client, which means users can see the content faster as there is no need to wait for JavaScript to load and render the page.

Better User Experience

With SSR, the initial page load is faster, and there are fewer loading spinners and flashes of unstyled content, resulting in a smoother user experience.

Introduction to Remix

Remix is a full-stack React framework that focuses on server-side rendering. It allows developers to write both backend and frontend code within a single application, simplifying the development process. Here are some of the core features of Remix:

Core Features

Server-Side Rendering (SSR)

Remix provides fully rendered pages on the initial load, leading to a fast and comprehensive user interface experience.

Routing

Remix enhances routing by nesting routes within the parent page, which helps in creating a hierarchical structure and optimizing the user navigation experience.

Nested Routes and Layouts
  • Nested Routes: Allows components to be nested within parent routes, optimizing component rendering.

  • Nested Layouts: Enables shared layouts across multiple routes, avoiding repetitive styles and enhancing modular design.

Fullstack Data Flow

Remix bridges the client and server sides by allowing developers to write both frontend and backend code in the same file, reducing the complexity of asynchronous code, caching, and bug management. This results in minimized loading spinners and enhanced user experience.

Data Loading

Utilizes loader functions and the useLoaderData hook to optimize data fetching and loading. This co-location of data logic simplifies the development process and enhances performance.

State Management

Supports integration with state management tools like Redux and Recoil but is optimized to store critical data, such as user tokens, in cookies due to SSR, ensuring consistent state management across page reloads and transitions.

Form Handling

Remix uses traditional server-side form handling methodologies with functions like action and loader, and introduces a Form component that extends the standard HTML form element for seamless server-side operations and progressive enhancement.

Benefits

  • Optimized User Experience: By delivering server-rendered pages and optimizing client-server interactions, Remix ensures faster initial load times and smoother navigation.

  • Simplified Development: Writing backend and frontend code in the same file, and the use of shared TypeScript types, streamline the development process.

  • Effective State Management: Simplifies state management in SSR applications by using cookies rather than relying on client-side storage mechanisms.

  • Enhanced Form Handling: The Form component maintains functionality even for users with limited JavaScript support, ensuring broader accessibility.

Introduction to TanStack Query

TanStack Query (formerly known as React Query) is a powerful tool for managing server-state in React applications. It simplifies data fetching, caching, synchronization, and more. Here are some key features of TanStack Query:

Key Features

Easy Asynchronous State Management

TanStack Query makes it easy to manage asynchronous state in your React application, providing hooks for fetching, caching, and updating data.

Optimized Data Fetching with Caching

By default, TanStack Query caches data and efficiently refetches it only when necessary, reducing the number of network requests and improving performance.

Built-in Support for Prefetching, Pagination, and Infinite Scrolling

It provides built-in support for advanced use cases such as prefetching data, handling pagination, and implementing infinite scrolling.

Setting Up TanStack Query with SSR in Remix

Initial Setup for Remix

To integrate TanStack Query with Remix, you need to set up the QueryClient and the QueryClientProvider in your Remix application. Here’s how you can do it:

app/root.tsx
import { Outlet } from '@remix-run/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

export default function MyApp() {
    const [queryClient] = React.useState(
        () => new QueryClient({
            defaultOptions: {
                queries: {
                    staleTime: 60 * 1000, // Avoids immediate refetching on the client
                },
            },
        }),
    )

    return (
        <QueryClientProvider client={queryClient}>
            <Outlet />
        </QueryClientProvider>
    )
}

SSR with Hydration in Remix Routes

SSR with Remix requires prefetching queries on the server and hydrating them on the client. Below is an example of how to handle this in a Remix route:

app/routes/posts.tsx
import { json } from '@remix-run/node'
import {
    dehydrate,
    HydrationBoundary,
    QueryClient,
    useQuery,
} from '@tanstack/react-query'

export async function loader() {
    const queryClient = new QueryClient()

    await queryClient.prefetchQuery({
        queryKey: ['posts'],
        queryFn: getPosts,
    })

    return json({ dehydratedState: dehydrate(queryClient) })
}

function Posts() {
    const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
    const { data: commentsData } = useQuery({
        queryKey: ['posts-comments'],
        queryFn: getComments,
    })

    return (
        <div>
            {/* Render posts and comments here */}
        </div>
    )
}

export default function PostsRoute() {
    const { dehydratedState } = useLoaderData<typeof loader>()
    return (
        <HydrationBoundary state={dehydratedState}>
            <Posts />
        </HydrationBoundary>
    )
}

Explanation

  1. Setting Up QueryClient: In app/root.tsx, we initialize a QueryClient with some default options and wrap the application with QueryClientProvider.

  2. Prefetching Data on the Server: In the loader function of app/routes/posts.tsx, we create a QueryClient, prefetch the required data using queryClient.prefetchQuery and dehydrate the state using dehydrate(queryClient).

  3. Hydrating Data on the Client: In the PostsRoute component, we use the HydrationBoundary to hydrate the data on the client side using the state provided by the loader.

This setup ensures that the data is fetched on the server, passed to the client, and hydrated without any additional network requests.

Best Practices for Integrating TanStack Query with Remix

Avoiding Shared QueryClient Instances

Each request should have its own QueryClient instance to avoid data leakage between different users. Creating a new QueryClient for each request ensures that the data cache is isolated per user session.

Prefetching Queries on the Server

Prefetching queries on the server side using queryClient.prefetchQuery improves performance by fetching the required data before rendering the page. This reduces the number of network requests made by the client and ensures that the data is readily available when the page loads.

Graceful Degradation for Errors

When fetching critical data that affects the response status (e.g., 404 or 500), use queryClient.fetchQuery and handle any errors appropriately in the loader. This ensures that the application can gracefully handle errors and provide a better user experience.

Handling Dependent Queries

Handle dependent queries by chaining fetchQuery and prefetchQuery calls. This ensures that dependent data is fetched in the correct order and reduces the likelihood of errors occurring due to missing data.

Memory Management Strategies

To manage memory effectively, avoid setting the gcTime to 0, as this can cause hydration errors. Manually clear the cache using queryClient.clear() if needed, and consider setting a shorter gcTime to manage server memory usage better.

Alternatives to Remix and TanStack Query for SSR

Although Remix and TanStack Query provide a powerful combination for SSR, there are other alternatives available. Here are some user opinions and alternatives based on discussions in the React community:

  • Next.js: Another popular React framework for SSR that offers built-in routing, API handling, and extensive documentation. Some users prefer Next.js for its ecosystem and community support.

  • Razzle: A framework that abstracts the configuration of SSR for React applications, making it easier to set up SSR without deep diving into Webpack and Babel configurations.

User experiences with these frameworks vary, and the choice depends on specific project requirements and developer preferences.

Case Study Examples and Use Cases

To understand the practical implementation of SSR with Remix and TanStack Query, let's explore a few real-world scenarios and use cases.

E-commerce Websites

E-commerce websites benefit significantly from SSR due to the need for fast page loads and SEO optimization. By using Remix for SSR, product pages can be server-rendered and delivered quickly to the user, improving their shopping experience. TanStack Query can manage the server-state, fetching product data and caching it to reduce load times on subsequent visits.

Example
import { useLoaderData } from '@remix-run/react'
import { json } from '@remix-run/node'
import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query'

async function getProduct(id) {
    // Fetch product data from an API
    const response = await fetch(`/api/products/${id}`)
    if (!response.ok) {
        throw new Error('Failed to fetch product')
    }
    return response.json()
}

export async function loader({ params }) {
    const queryClient = new QueryClient()
    await queryClient.prefetchQuery(['product', params.id], () => getProduct(params.id))
    return json({ dehydratedState: dehydrate(queryClient) })
}

function ProductPage() {
    const { id } = useLoaderData()
    const { data: product } = useQuery(['product', id], () => getProduct(id))

    return (
        <div>
            {/* Render product details */}
            <h1>{product.name}</h1>
            <p>{product.description}</p>
        </div>
    )
}

export default function ProductRoute() {
    const { dehydratedState } = useLoaderData()
    return (
        <HydrationBoundary state={dehydratedState}>
            <ProductPage />
        </HydrationBoundary>
    )
}

Content Management Systems (CMS)

For CMS applications, SSR ensures that content is rendered quickly and is easily indexable by search engines. Remix can be used to fetch and render content on the server, while TanStack Query handles the data fetching and caching.

Example
import { useLoaderData } from '@remix-run/react'
import { json } from '@remix-run/node'
import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query'

async function getArticles() {
    // Fetch articles from an API
    const response = await fetch('/api/articles')
    if (!response.ok) {
        throw new Error('Failed to fetch articles')
    }
    return response.json()
}

export async function loader() {
    const queryClient = new QueryClient()
    await queryClient.prefetchQuery('articles', getArticles)
    return json({ dehydratedState: dehydrate(queryClient) })
}

function ArticlesPage() {
    const { data: articles } = useQuery('articles', getArticles)

    return (
        <div>
            <h1>Articles</h1>
            <ul>
                {articles.map(article => (
                    <li key={article.id}>{article.title}</li>
                ))}
            </ul>
        </div>
    )
}

export default function ArticlesRoute() {
    const { dehydratedState } = useLoaderData()
    return (
        <HydrationBoundary state={dehydratedState}>
            <ArticlesPage />
        </HydrationBoundary>
    )
}

Progressive Web Apps (PWAs)

PWAs aim to provide a native app-like experience on the web. Using SSR with Remix can enhance the initial load performance and ensure that the app is accessible even with limited JavaScript support. TanStack Query can manage the data fetching and state synchronization to provide a seamless user experience.

Example
import { useLoaderData } from '@remix-run/react'
import { json } from '@remix-run/node'
import { QueryClient, dehydrate, useQuery } from '@tanstack/react-query'

async function getUserProfile() {
    // Fetch user profile data from an API
    const response = await fetch('/api/user/profile')
    if (!response.ok) {
        throw new Error('Failed to fetch user profile')
    }
    return response.json()
}

export async function loader() {
    const queryClient = new QueryClient()
    await queryClient.prefetchQuery('userProfile', getUserProfile)
    return json({ dehydratedState: dehydrate(queryClient) })
}

function UserProfilePage() {
    const { data: userProfile } = useQuery('userProfile', getUserProfile)

    return (
        <div>
            <h1>User Profile</h1>
            <p>{userProfile.name}</p>
            <p>{userProfile.email}</p>
        </div>
    )
}

export default function UserProfileRoute() {
    const { dehydratedState } = useLoaderData()
    return (
        <HydrationBoundary state={dehydratedState}>
            <UserProfilePage />
        </HydrationBoundary>
    )
}

Debugging and Error Handling

When working with SSR and TanStack Query, it's crucial to handle errors gracefully. Here’s an example of handling errors in the loader function:

import { json } from '@remix-run/node'
import { QueryClient, dehydrate } from '@tanstack/react-query'

export async function loader() {
    const queryClient = new QueryClient()
    let data
    try {
        data = await queryClient.fetchQuery('data', fetchData)
    } catch (error) {
        // Handle error appropriately
        throw new Response('Failed to load data', { status: 500 })
    }
    return json({ dehydratedState: dehydrate(queryClient), data })
}

function DataPage() {
    const { data } = useLoaderData()
    return <div>{/* Render data here */}</div>
}

export default function DataRoute() {
    const { dehydratedState } = useLoaderData()
    return (
        <HydrationBoundary state={dehydratedState}>
            <DataPage />
        </HydrationBoundary>
    )
}

This ensures that any critical errors during data fetching are handled gracefully, and the user is presented with an appropriate error message.

Serialization and Security Considerations

Proper serialization of the dehydratedState is crucial to prevent cross-site scripting (XSS) vulnerabilities. Instead of using raw JSON.stringify, consider using safer libraries like superjson or devalue.

Example

import superjson from 'superjson'
import { json } from '@remix-run/node'
import { QueryClient, dehydrate } from '@tanstack/react-query'

export async function loader() {
    const queryClient = new QueryClient()
    await queryClient.prefetchQuery('data', fetchData)
    const dehydratedState = dehydrate(queryClient)
    const serializedState = superjson.stringify(dehydratedState)
    return json({ dehydratedState: serializedState })
}

Conclusion

Integrating SSR with Remix and TanStack Query offers numerous benefits, including improved SEO, faster initial page load times, and a better overall user experience. By following best practices and leveraging the powerful features of both Remix and TanStack Query, developers can build highly optimized and performant web applications.

As demonstrated in the examples and use cases, this combination is suitable for a variety of applications, from e-commerce websites to progressive web apps. The code snippets and guidelines provided should serve as a solid foundation for implementing SSR in your projects.

Embrace the power of server-side rendering with Remix and TanStack Query to create fast, reliable, and SEO-friendly web applications. Happy coding!

Raymond Yeh

Raymond Yeh

Published on 29 October 2024

Get engineers' time back from marketing!

Don't let managing a blog on your site get in the way of your core product.

Wisp empowers your marketing team to create and manage content on your website without consuming more engineering hours.

Get started in few lines of codes.

Choosing a CMS
Related Posts
Understanding SSR in Remix and Its Benefits

Understanding SSR in Remix and Its Benefits

Uncover the power of SSR in Remix! Improve performance, SEO, and user experience with this cutting-edge React framework that rivals Next.js. Dive into its unique features and benefits.

Read Full Story
Should I Use TanStack Query When Most of My Components Are Server Components?

Should I Use TanStack Query When Most of My Components Are Server Components?

Learn why TanStack Query remains crucial in a server-component-heavy architecture and see its impact on UI performance and developer efficiency.

Read Full Story
How to Bootstrap a Remix Project

How to Bootstrap a Remix Project

From Zero to Full-Stack: Mastering Remix and Prisma Discover how to build fast, scalable, and SEO-friendly web applications with practical examples and easy integration tips.

Read Full Story