Files
litreddit/frontend/notes.md
T
2026-06-24 14:20:05 +02:00

6.4 KiB

All pages are server components by default. Only pages with 'use-client' on top are client components.

Server components fetch data on the server and pre-render the HTML. For example, in the following page:

import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
 
export default async function Page({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
 
  return (
    <div>
      <main>
        <h1>{post.title}</h1>
        {/* ... */}
        <LikeButton likes={post.likes} />
      </main>
    </div>
  )
}
'use client'
 
import { useState } from 'react'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

The getPost function is run server-side to get the data and render the whole div tag except for the LikeButton which is rendered client-side.

Using any hooks e.g. useQuery or useMutation requires us to do so inside a client component.

We can also have Next.js fetches data and pre-renders HTML at build time and on periodical revalidations using Incremental Static Regeneration (ISR) as follows:

interface Post {
  id: string
  title: string
  content: string
}
 
// Next.js will invalidate the cache when a
// request comes in, at most once every 60 seconds.
export const revalidate = 60
 
// For each `post` in the array returned by `generateStaticParams`, 
// we will pre-render and cache a corresponding page at build time and on later revalidations.
// If a request comes in for a path that hasn't been generated,
// i.e. if there is a request for a post not retrieved when `generateStaticParams` was last run,
// Next.js will server-render the page on-demand.
export const dynamicParams = true // or false, to 404 on unknown paths
 
export async function generateStaticParams() {
  const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json()
  )
  return posts.map((post) => ({
    id: String(post.id),
  }))
}
 
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then(
    (res) => res.json()
  )
  return (
    <main>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </main>
  )
}

SSR with @apollo/client:

import { Wrapper } from '@/components'
import { SESSION_COOKIE_NAME } from '@/constants'
import { MeDocument, PostDocument } from '@/generated/graphql/graphql'
import { createApolloClient } from '@/lib'
import { Heading, Text } from '@chakra-ui/react'
import { cookies } from 'next/headers'
import { ClientSection } from './ClientSection'

interface Props {
  params: {
    id: string
  }
}

const PostPage: React.FC<Props> = async ({ params: { id } }) => {
  const cookieStore = cookies()
  const cookie = cookieStore.get(SESSION_COOKIE_NAME)?.value
  const apollo = await createApolloClient(cookie)
  const { data } = await apollo.query({ query: PostDocument, variables: { id } })
  const { data: meData } = await apollo.query({ query: MeDocument })

  return (
    <Wrapper>
      {
        data?.post ?
          <>
            <Heading as='h3' size='md'>{data.post.title}</Heading>
            <Text>Posted by {data.post.author.username}</Text>
            <Text mt={4}>{data.post.content}</Text>
            {
              data?.post?.authorID == meData?.me?.id &&
              <ClientSection data={data} />
            }
          </>
          :
          <>Post not found!</>
      }
    </Wrapper>
  )
}
export default PostPage

where createApolloClient is:

'use server'
import { SESSION_COOKIE_NAME } from '@/constants'
import { from, HttpLink, NormalizedCacheObject } from '@apollo/client'
import { onError } from '@apollo/client/link/error'
import { ApolloClient, InMemoryCache, registerApolloClient } from '@apollo/experimental-nextjs-app-support'


const errorLink = onError(({ graphQLErrors, networkError }) => {
  graphQLErrors?.forEach(({ message, locations, path }) => {
    console.log(
      `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
    )
  })
  if (networkError) console.error(`[Network error]: ${networkError}`)
})


// This is the server-side client
export const createApolloClient = async (cookie?: string) => registerApolloClient<ApolloClient<NormalizedCacheObject>>(() => {
  const httpLink = new HttpLink({
    uri: process.env.NEXT_PUBLIC_BACKEND_URI,
    credentials: 'include',
    headers: {
      cookie: cookie ? `${SESSION_COOKIE_NAME}=${cookie}` : ''
    }
  })
  return new ApolloClient({
    ssrMode: true,
    link: from([errorLink, httpLink]),
    cache: new InMemoryCache()
  })
})

If we use react-query, we can combine the initial server-side data fetching and subsequent useQuery calls as follows:

// app/posts/page.tsx
import {
  dehydrate,
  HydrationBoundary,
  QueryClient,
} from '@tanstack/react-query'
import Posts from './posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()


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

  return (
    // Hydration is when React converts the pre-rendered HTML from the server into a fully interactive application by attaching event handlers.
    // Dehydration, on the other hand, is when an interactive JavaScript object is converted into HTML.
    // Here we are converting the queryClient which contains data fetched server-side into HTML and passing that HTML into HydrationBoundary, which is a Client component.
    // When HydrationBoundary is rendered client-side, the HTML will be hydrated, i.e. the data fetched server-side will be integrated with useQuery seamlessly.
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}
// app/posts/posts.tsx
'use client'

export default function Posts() {
  // This useQuery could just as well happen in some deeper
  // child to <Posts>, data will be available immediately either way
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  })

  // This query was not prefetched on the server and will not start
  // fetching until on the client, both patterns are fine to mix.
  const { data: commentsData } = useQuery({
    queryKey: ['posts-comments'],
    queryFn: getComments,
  })

  // ...
}