Classic web pages usually render visual elements on the screen using HTML (to structure the visual elements) and CSS (to style them), while utilizing JavaScript just to make that page interactive (e.g. trigger an action when the user clicks a button, perform error handling for forms, display additional information on hover, etc.).
In contrast, Single Page Applications or SPAs rely heavily on JavaScript for almost everything, including rendering the entire user interface (UI). That is also the case for almost every web development framework (also simply called JavaScript frameworks) out there, including Angular, Vue, and React. React is a telling example, as it uses JavaScript for writing every part of the component, including the template (JSX) and the styling (more on that in future tutorials). When you build a React application to make it ready for production, the JSX template, the styling of the components, and the JS logic are transformed using a compiler (usually Babel) and bundled together into pure JS code and optimized using a dedicated build tool (usually Webpack). The resulting JS resources together with minimal HTML and CSS boilerplate are then served to the browser when the latter performs a GET request.
An advantage of this approach is that the server's response to a client request comprises minimal HTML and CSS code and some JavaScript resources that are usually obfuscated and heavily minified. That leads to faster response times from the server and a pleasing user experience (UX), as the visual elements start to be rendered dynamically as soon as the browser starts executing the JavaScript received from the server. Moreover, when the user navigates through the app, a few requests are further sent to the server as the majority of new visual elements are displayed in the browser by executing even more JavaScript written in the same resources that were initially fetched when the page was initialized.
The big disadvantage however, is that crawlers don't like SPAs. That means that when Google's bots visit your page to index it and then rank it for search results, they usually don't like to wait for the loading and execution of JS code so they can understand what visual elements are in it. And even if the crawlers have become increasingly more performant and JS-tolerant, there is still a problem with semantics. JavaScript frameworks were not designed with semantics in mind. That means that the HTML elements that are dynamically generated at runtime usually don't respect the best practices (e.g. use paragraphs for text content, use headers in hierarchical order, include semantic elements, etc.). That is a big red flag for SEO and solving it just by using React is pretty difficult.
To solve the discussed issues, we can use pre-rendering. That is generating HTML and CSS code in advance, for each page instead of relying solely on the client-side execution of JavaScript. As a result, the amount of JS code executed by the browser is heavily reduced as it is used only for making the page fully interactive. This process is called hydration and a great tool for doing this in React is Next.js.
There are usually 2 types of pre-rending, both being supported by Next.js.
In this case, the HTML and CSS code is generated on each request. This is an SEO-friendly approach that is hybrid in nature, as it allows the application to serve different content in a context-dependent fashion while minimizing the amount of JavaSript code that gets executed on the client. Hence, SSR is perfect for scenarios where real-time interactivity (e.g. a live map, live stats display, counter, etc.) is not required, but some parts of the page cannot be determined at build time, but on each HTTP request.
SSG goes one step further and pre-renders the styled HTML at build time. That means that the same content will be returned for every subsequent HTTP request and data on that page will not change whatsoever. This approach is suitable for landing pages, articles, blog posts, or other web pages that solely rely on static content. This pre-rendering technique is highly recommended as it offers huge SEO benefits, but it can also be leveraged by Content Delivery Networks (CDNs) or other caching techniques (e.g. Cloudflare).
Next.js 15 goes one step further and instead of looking at the pages as SSG/SSR it instead using new react concepts as Server Components and Client Components. In Next.js 15, all your components are by default server components, which means Next.js will try to populate them when rendering the page before sending it to the client's browser. Since all components are by default server components and are rendered on the server, there is no hydration step necessary (it still takes place when possible) and your pages will work even with javascript disabled. In these situations the data fetching happens on the server. To find out more about the advantages of server components read the Server Components page.
On the other hand if your page needs some level of interactivity using javascript/react on the client side (and you need to make use of methods like useState/useEffect) you will need to upgrade your component to a Client Component. In these components you are allowed to import from the react module, in server components you are not.
It is always recommended to use serverside components unless you need actual interactive components on the client side. For example imagine a blog written in Nextjs with react. Almost all pages will be static with (latest articles page, single article page with comment section, etc.), except for a few widgets relevant to the logged in user. In this situation we would design all our pages as static components, rendered fully on the server with links sending to the user to another page. For example, if the hydration step takes place, nextjs will considered the page “enhanced” and request a smaller amount of data to populate the new page (of the article, when coming form the home page), on the other hand if the hydration did not take place (due to a variety of factors like: js disabled, low internet speed and js did not load yet) clicking the link will redirect the browser to the article's URL which in change will request a new full page render from the server. This way, server components end up supporting both situations when the page hydrates or not. That is why the default in nextjs 15 is server components.
For a more in-depth comparison between server and client components please read Server and Client Composition Patterns
Next.js has an opinionated way of handling the application routes. In order to define new website pages you need to create a page.tsx file for each in a int app folder or in a sub folder representing the path you want to implement
* /app/page.tsx //- will define the hopage * /app/about/page.tsx //- will define the page located at domain.com/about
You can find more information in the Pages section
Next.js also supports **dynamic routes** which are useful when the route depends on a URI parameter. Here are some examples of URLs where this might come in handy:
* /blog/:post-id * /profile/:user-id * /videos/:video-id/statistics * /questions/*
In these cases, square brackets are used in the naming of files or directories to indicate that the route is dynamic:
* /app/article/[article-id]/page.tsx * /app/user/[user-id]/page.tsx * /app/videos/[video-id]/statistics.tsx * /app/questions/[...all].jsx
To link pages together, Next.js provides you with a dedicated component called Link, which works similarly to the classic anchor tag.
import Link from 'next/link'; function Home({ otherBlogPosts }) { return ( <ul> <li> <Link href="/">Home</Link> </li> <li> <Link href="/blog">Thoughts</Link> </li> <li> <Link href="/blog/first-post">How the story beginned</Link> </li> {otherBlogPosts.map((post) => ( <li key={post.id}> <Link href={`/blog/${encodeURIComponent(post.path)}`}> {post.name} </Link> </li> ))} </ul> ) } export default Home;
As an alternative, if you want to navigate between pages dynamically in a client component, from your JavaScript logic, you can do that with useRouter.
'use client' import { useRouter } from 'next/navigation'; function Blog({ completePostpath }) { const router = useRouter(); const handleClick = () => router.push(completePostpath); // e.g. /blog/first-post return ( <div> <p>Welcome!</p> <button onClick={handleClick}>My first blog post</button> </div> ) } export default Blog;
If you want to reuse components on multiple pages, **layouts** come in handy. For instance, your app might have a header and a footer on each page:
// app/article/layout.tsx // this layout will be the layout for all pages in the article folder and it's children import Navbar from '..components/navbar'; import Footer from '..components/footer'; export default function ArticleLayout({ children }) { return ( <> <Navbar /> <main>{children}</main> <Footer /> </> ); }
Multiple layouts can also be used on a per-page basis. You can find more about that here.
In both server and client components data is fetched using the native fetch method. That being said it is recommended to avoid using it directly in combination with fetch inside client components but rather use a React library optimized for this pattern like SWR which adds support for use SWR
// server side component fetch export default async function Page() { let data = await fetch('https://api.vercel.app/blog') let posts = await data.json() return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) }
// client side component fetch with useEffect 'use client' import { useState, useEffect } from 'react' export function Posts() { const [posts, setPosts] = useState(null) useEffect(() => { async function fetchPosts() { let res = await fetch('https://api.vercel.app/blog') let data = await res.json() setPosts(data) } fetchPosts() }, []) if (!posts) return <div>Loading...</div> return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) }
//swr import useSWR from 'swr' function Profile() { const { data, error, isLoading } = useSWR('/api/user', fetcher) if (error) return <div>failed to load</div> if (isLoading) return <div>loading...</div> return <div>hello {data.name}!</div> }
Server Actions are asynchronous functions that are executed on the server. They can be called in Server and Client Components to handle form submissions and data mutations in Next.js applications.
Server actions can be used in forms with the action attribute <form action={serverAction}></form>, which will call your serverAction(formData: FormData} function and pass all the forms inputs to it.
// Inside server components, actions can be coded directly as // a function as long as the 'user server' directive is written, // telling next.js that the given function should run only on the server. // This example does not use the data to process it in any way, // it is just showing you how you can process the data // It is recommended unless the code is trivial and small, // to put all actions in a separate file implementing the logic of the components export default function Page() { async function createInvoice(formData: FormData) { 'use server' const rawFormData = { customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), } // mutate data // revalidate cache } return <form action={createInvoice}>...</form> }
While actions can't be used directly inside a client component, they can be defined in a separate file and then used directly in the component. Behind the scenes, this will send a fetch request to the server, execute the function on the server, and return the result to the browser.
// app/actions.ts 'use server' export async function create() { console.log('button clicked') } //This code will run on the server and show in the server console //Nothing will be written in the browser's console
// app/coomponents/some-button.tsx 'use client' import { create } from '@/app/actions' export function Button() { return <button onClick={() => create()}>Create</button> }
There is a plethora of configurations supported by Next.js that can be written in the next.config.js file which resides at the root of your project. One can set environment variables, base paths for routes, internationalization, CDN support, path reroutes, etc. You can find more about that here.
Please take a minute to fill in the feedback form for this lab.