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

222 lines
6.4 KiB
Markdown

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:
```tsx
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>
)
}
```
```tsx
'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)**](https://nextjs.org/docs/app/guides/incremental-static-regeneration) as follows:
```tsx
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`:
```tsx
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:
```tsx
'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:
```tsx
// 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>
)
}
```
```tsx
// 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,
})
// ...
}
```