Ultimate Beginner's Tutorial: Getting Started with Next.js 14

Ultimate Beginner's Tutorial: Getting Started with Next.js 14

Next.js for Newcomers: A Clear Guide to Essentials

Featured on Hashnode
This post contains affiliate links. If you make a purchase through these links, I may earn a commission at no additional cost to you. Your support helps me continue creating valuable content. Thank you!

Are you looking to level up your web development skills? If you're familiar with React and want to build fast, SEO-friendly web applications, Next.js 14 is your next stop. In this beginner's guide, we'll dive into the latest version of this powerful React framework and show you why it's become a go-to choice for developers worldwide.

Next.js, developed by Vercel, is a React framework that enables you to create full-stack web applications with ease. It simplifies many complex tasks in web development, from routing to server-side rendering, allowing you to focus on building your application rather than configuring tools.

Version 14, released in October 2023, brings significant improvements and new features that make Next.js even more powerful and developer-friendly. With a focus on performance, developer experience, and flexibility, Next.js 14 introduces enhancements like improved Server Components, a more intuitive App Router, and built-in optimizations that can dramatically speed up your development process and application performance.

Whether you're a React developer looking to expand your toolkit or a beginner eager to start with a robust framework, this guide will walk you through the basics of Next.js 14. We'll cover everything from setting up your first project to creating dynamic pages, all while explaining the core concepts that make Next.js stand out in the world of web development.

Ready to get started? Let's dive in and explore the exciting world of Next.js 14!

Key features of NextJS

Next.js 14 builds upon its predecessors, introducing and refining features that make it a powerful tool for modern web development. Let's explore the key features that set this version apart:

a) Server Components as Default

Next.js 14 embraces React Server Components, making them the default. This shift allows for improved performance and a more efficient rendering strategy:

  • Reduced JavaScript sent to the client, resulting in faster page loads

  • Ability to access backend resources directly without additional API layers

  • Improved initial page load and time-to-interactive metrics

b) Enhanced App Router

The App Router, introduced in version 13, has been further refined in Next.js 14:

  • More intuitive file-based routing system

  • Improved nested layouts and easier management of shared UI elements

  • Built-in support for loading states and error handling at the route level

c) Improved Performance

Next.js 14 comes with several performance enhancements:

  • Partial Prerendering: A hybrid rendering solution that combines static and dynamic content more efficiently

  • Improved static and dynamic rendering strategies

  • Enhanced image and font optimization

d) Built-in Optimizations

Next.js 14 includes several out-of-the-box optimizations:

  • Automatic code splitting for faster page loads

  • Improved prefetching strategies for smoother navigation

  • Built-in CSS and JavaScript minification

e) Turbopack (Beta)

While still in beta, Turbopack offers significant improvements in development speed:

  • Faster refresh rates during development

  • Improved build times for large applications

f) Simplified Data Fetching

Next.js 14 streamlines data fetching with Server Components:

  • Native fetch support with automatic request deduplication

  • Simplified API for handling loading and error states

These features combine to make Next.js 14 a robust, efficient, and developer-friendly framework for building modern web applications. As we progress through this guide, we'll explore how to leverage these features in your projects, starting with setting up your development environment in the next section.

Setting up your development environment

Before we dive into building with Next.js 14, let's set up your development environment. This process involves installing the necessary tools and creating your first Next.js project.

a) Installing Node.js and npm

Next.js requires Node.js 18.17 or later. To check if you have Node.js installed, run this command in your terminal:

node --version

If Node.js isn't installed or you need to update, visit the official Node.js website (https://nodejs.org) and download the latest LTS version.

b) Creating a New Next.js 14 Project

With Node.js installed, you can create a new Next.js project using the following command:

npx create-next-app@latest my-nextjs-app

This command will prompt you with a series of questions. Here are the recommended answers for this tutorial:

Would you like to use TypeScript? › No
Would you like to use ESLint? › Yes
Would you like to use Tailwind CSS? › No
Would you like to use `src/` directory? › No
Would you like to use App Router? (recommended) › Yes
Would you like to customize the default import alias? › No

c) Navigate to Your Project Directory

Once the installation is complete, navigate to your project directory:

cd my-nextjs-app

d) Starting the Development Server

To start your development server, run:

npm run dev

This will start your Next.js app on http://localhost:3000.

e) Project Structure Overview

Let's take a look at the basic structure of your Next.js 14 project:

my-nextjs-app/
├── app/
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.js
│   └── page.js
├── public/
│   └── next.svg
│   └── vercel.svg
├── .eslintrc.json
├── .gitignore
├── next.config.js
├── package.json
└── README.md

Key files and directories:

  • app/: This is where your application code lives. The page.js file in this directory corresponds to the route /.

  • public/: Static assets like images go here.

  • next.config.js: Configuration file for Next.js.

  • package.json: Project dependencies and scripts.

f) Your First Next.js Component

Open app/page.js. This is your home page component. Let's simplify it to a basic "Hello World" example:

export default function Home() {
  return (
    <main>
      <h1>Welcome to Next.js 14!</h1>
      <p>Hello, World!</p>
    </main>
  );
}

Save this file and check your browser at http://localhost:3000. You should see your "Hello World" message.

With your development environment set up and your first component created, you're ready to start exploring the more advanced features of Next.js 14. In the next section, we'll dive deeper into the App Router and how it simplifies the creation and organization of your application's pages.

Understanding the App Router

Next.js 14 uses the App Router, a powerful and intuitive routing system that leverages the file system to define routes. This approach simplifies the process of creating and organizing pages in your application.

File-Based Routing

In the App Router, each folder represents a route segment and each page.js file within these folders becomes a publicly accessible route.

For example:

app/
├── page.js          // Renders the '/' route
├── about/
│   └── page.js      // Renders the '/about' route
└── blog/
    ├── page.js      // Renders the '/blog' route
    └── [slug]/
        └── page.js  // Renders dynamic routes like '/blog/my-post'

Creating Your First Pages

Let's create a few basic pages to demonstrate how routing works in Next.js 14.

  1. Home Page (already exists):
// app/page.js
export default function Home() {
  return (
    <main>
      <h1>Welcome to My Next.js 14 Site</h1>
      <p>This is the home page.</p>
    </main>
  );
}
  1. About Page:

Create a new file at app/about/page.js:

// app/about/page.js
export default function About() {
  return (
    <main>
      <h1>About Us</h1>
      <p>Learn more about our company here.</p>
    </main>
  );
}
  1. Blog Page:

Create a new file at app/blog/page.js:

// app/blog/page.js
export default function Blog() {
  return (
    <main>
      <h1>Our Blog</h1>
      <p>Read our latest articles here.</p>
    </main>
  );
}

Nested Routes and Layouts

The App Router allows you to create nested routes and shared layouts easily.

  1. Create a layout for the blog section:

Create a new file at app/blog/layout.js:

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div>
      <nav>
        <h2>Blog Navigation</h2>
        {/* Add blog navigation items here */}
      </nav>
      {children}
    </div>
  );
}

This layout will be applied to all pages under the /blog route.

  1. Create a dynamic blog post page:

Create a new file at app/blog/[slug]/page.js:

// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
  return (
    <article>
      <h1>Blog Post: {params.slug}</h1>
      <p>This is the content of the blog post.</p>
    </article>
  );
}

This page will handle dynamic routes like /blog/my-first-post.

Linking Between Pages

To create links between your pages, use the Link component from Next.js:

// app/page.js
import Link from 'next/link';

export default function Home() {
  return (
    <main>
      <h1>Welcome to My Next.js 14 Site</h1>
      <nav>
        <ul>
          <li><Link href="/about">About Us</Link></li>
          <li><Link href="/blog">Blog</Link></li>
        </ul>
      </nav>
    </main>
  );
}

The App Router in Next.js 14 provides a powerful and flexible way to structure your application. By leveraging file-based routing and nested layouts, you can create complex applications with clean, organized code. In the next section, we'll explore how to work with Server and Client Components to optimize your application's performance.

Server and Client Components

Next.js 14 leverages React Server Components, which offer a new way to build applications that combine the benefits of server-side rendering with the interactivity of client-side apps. Understanding the difference between Server and Client Components is crucial for optimizing your Next.js application.

Explaining the Difference

Server Components:

  • Render on the server

  • Reduce the amount of JavaScript sent to the client

  • Can directly access backend resources

  • Cannot use browser-only APIs or React hooks

Client Components:

  • Render on the client (browser)

  • Enable interactivity and use of React hooks

  • Can access browser APIs

  • Increase the JavaScript bundle size

When to Use Each Type

Use Server Components for:

  • Fetching data

  • Accessing backend resources directly

  • Keeping sensitive information on the server

  • Large dependencies that don't need client-side interactivity

Use Client Components for:

  • Interactivity and event listeners

  • Using React hooks (useState, useEffect, etc.)

  • Browser-only APIs

  • Custom hooks that depend on state or effects

Converting Between Server and Client Components

By default, all components in the app directory are Server Components. To make a component a Client Component, add the 'use client' directive at the top of the file.

Example: Server Component

// app/ServerComponent.js
import { getServerData } from './utils/data';

export default async function ServerComponent() {
  const data = await getServerData();

  return (
    <div>
      <h2>Server Component</h2>
      <p>Data: {data}</p>
    </div>
  );
}

Example: Client Component

'use client'

// app/ClientComponent.js
import { useState } from 'react';

export default function ClientComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h2>Client Component</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Mixing Server and Client Components

You can use Client Components within Server Components, but not vice versa. Here's an example of how to combine them:

// app/page.js
import ServerComponent from './ServerComponent';
import ClientComponent from './ClientComponent';

export default function Page() {
  return (
    <div>
      <h1>My Page</h1>
      <ServerComponent />
      <ClientComponent />
    </div>
  );
}

Best Practices

  1. Start with Server Components and add Client Components as needed for interactivity.

  2. Keep Client Components lean to minimize the JavaScript sent to the browser.

  3. Use Server Components for data fetching and passing props to Client Components.

  4. Create clear boundaries between Server and Client Components in your application structure.

Understanding and effectively using Server and Client Components allows you to build performant and interactive applications with Next.js 14. By leveraging the strengths of each component type, you can optimize both the user experience and the development process.

In the next section, we'll explore how to handle data fetching in Next.js 14, taking advantage of the new capabilities offered by Server Components.

Basic Data Fetching

Next.js 14 introduces powerful new ways to fetch data, especially when using Server Components. This section will cover the basics of data fetching, implementing loading states, and handling errors.

Using Server Components for Data Fetching

Server Components can fetch data directly on the server, which offers several advantages:

  • Improved performance by reducing client-side JavaScript

  • Direct access to data sources without exposing sensitive information to the client

  • Automatic request deduplication

Here's an example of data fetching in a Server Component:

// app/users/page.js
async function getUsers() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  if (!res.ok) throw new Error('Failed to fetch users');
  return res.json();
}

export default async function UsersPage() {
  const users = await getUsers();

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Note that we can use async/await directly in the component, as it's a Server Component.

Implementing Loading States

Next.js 14 provides an elegant way to handle loading states using a special loading.js file:

// app/users/loading.js
export default function Loading() {
  return <div>Loading users...</div>;
}

This loading component will be shown automatically while the UsersPage component is fetching data.

Error Handling

For error handling, you can create an error.js file in the same directory:

// app/users/error.js
'use client' // Error components must be Client Components

export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

If an error occurs in the UsersPage component, this error component will be displayed.

Data Fetching in Client Components

While Server Components are preferred for data fetching, sometimes you need to fetch data on the client. Here's how you can do that:

'use client'

// app/client-users/page.js
import { useState, useEffect } from 'react';

export default function ClientUsersPage() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUsers() {
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/users');
        if (!res.ok) throw new Error('Failed to fetch users');
        const data = await res.json();
        setUsers(data);
      } catch (error) {
        console.error('Error fetching users:', error);
      } finally {
        setLoading(false);
      }
    }

    fetchUsers();
  }, []);

  if (loading) return <div>Loading users...</div>;

  return (
    <div>
      <h1>Users (Client-side)</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Static Data Fetching

For data that doesn't change often, you can use static data fetching:

// app/static-users/page.js
export async function generateStaticParams() {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();
  return users.map(user => ({ id: user.id.toString() }));
}

async function getUser(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (!res.ok) throw new Error('Failed to fetch user');
  return res.json();
}

export default async function UserPage({ params }) {
  const user = await getUser(params.id);

  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
    </div>
  );
}

This approach will generate static pages for each user at build time.

Next.js 14's data fetching capabilities provide a flexible and powerful way to handle various data scenarios in your application. By leveraging Server Components and the built-in loading and error-handling features, you can create robust and performant applications with ease.

In the next section, we'll explore how to build dynamic pages using the knowledge we've gained so far.

Building Your First Dynamic Page

Dynamic pages are a crucial part of many web applications. They allow you to create reusable page templates that can display different content based on parameters in the URL. In this section, we'll create a simple blog post page that demonstrates how to use dynamic routes in Next.js 14.

Creating a Dynamic Route

First, let's set up a dynamic route for our blog posts:

  1. Create a new folder structure: app/blog/[slug]

  2. Inside this folder, create a page.js file

Your folder structure should look like this:

app/
├── blog/
│   ├── [slug]/
│   │   └── page.js
│   └── page.js

Implementing the Dynamic Page

Now, let's implement our dynamic blog post page:

// app/blog/[slug]/page.js
async function getPost(slug) {
  // In a real application, you would fetch the post data from an API or database
  // This is a mock function for demonstration purposes
  return {
    slug,
    title: `Post about ${slug}`,
    content: `This is the content of the post about ${slug}.`
  };
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

This component will render different content based on the slug parameter in the URL.

Generating Static Params (Optional)

If you want to pre-render some or all of your blog posts at build time, you can use generateStaticParams:

// app/blog/[slug]/page.js

// ... previous code ...

export async function generateStaticParams() {
  // In a real application, you would fetch this list from your CMS or database
  const posts = ['first-post', 'second-post', 'third-post'];

  return posts.map((slug) => ({
    slug: slug,
  }));
}

This function tells Next.js which paths to pre-render at build time.

Creating a Blog Index Page

To link to our dynamic blog posts, let's create a simple blog index page:

// app/blog/page.js
import Link from 'next/link';

export default function BlogIndex() {
  // In a real application, you would fetch this list from your CMS or database
  const posts = [
    { slug: 'first-post', title: 'My First Post' },
    { slug: 'second-post', title: 'My Second Post' },
    { slug: 'third-post', title: 'My Third Post' },
  ];

  return (
    <div>
      <h1>My Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link href={`/blog/${post.slug}`}>
              {post.title}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

Handling Not Found Pages

To handle cases where a blog post doesn't exist, you can create a not-found.js file:

// app/blog/[slug]/not-found.js
export default function NotFound() {
  return (
    <div>
      <h2>Not Found</h2>
      <p>Could not find requested resource</p>
    </div>
  );
}

Then, update your BlogPost component to use it:

// app/blog/[slug]/page.js
import { notFound } from 'next/navigation';

async function getPost(slug) {
  // Simulate fetching post data
  const posts = {
    'first-post': { title: 'My First Post', content: 'Content of first post' },
    'second-post': { title: 'My Second Post', content: 'Content of second post' },
  };
  return posts[slug];
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Now, if a user tries to access a non-existent blog post, they'll see the "Not Found" page.

Testing Your Dynamic Page

You can now test your dynamic blog post pages by navigating to URLs like:

Each URL should display different content based on the slug, or show the "Not Found" page for non-existent posts.

By implementing dynamic routes, you've created a flexible system for handling blog posts or any other type of content that follows a similar pattern. This approach can be extended to handle more complex scenarios, such as nested dynamic routes or routes with multiple parameters.

In the next and final section, we'll wrap up our guide with some next steps and additional resources for continuing your Next.js 14 journey.

Next Steps and Resources

Congratulations! You've taken your first steps into the world of Next.js 14. You've learned about setting up a project, working with the App Router, understanding Server and Client Components, fetching data, and creating dynamic pages. But this is just the beginning of what you can do with Next.js. Here are some next steps and resources to continue your journey:

Deepen Your Knowledge

  1. Advanced Routing: Explore more complex routing scenarios, including catch-all routes and optional catch-all routes.

  2. API Routes: Learn how to create API endpoints within your Next.js application using the app/api directory.

  3. Authentication: Implement user authentication in your Next.js app using solutions like NextAuth.js.

  4. State Management: Explore state management solutions that work well with Next.js, such as React Context API or libraries like Redux or Zustand.

  5. Testing: Learn how to write and run tests for your Next.js applications using tools like Jest and React Testing Library.

Performance Optimization

  1. Image Optimization: Dive deeper into Next.js's built-in Image component for automatic image optimization.

  2. Font Optimization: Explore Next.js's font optimization features to improve loading times and prevent layout shift.

  3. Lazy Loading: Implement lazy loading for components and modules to improve initial page load times.

Deployment and Production

  1. Deployment: Learn how to deploy your Next.js application to platforms like Vercel, Netlify, or your own server.

  2. Environment Variables: Understand how to use environment variables for sensitive information and configuration.

  3. Monitoring and Analytics: Implement monitoring and analytics to track your application's performance and user behavior.

Official Resources

Community Resources

  • Next.js Discord: Join the community to ask questions and share knowledge.

  • Stack Overflow: A great place to find solutions to specific problems.

  • DEV Community: Read articles and tutorials from other developers working with Next.js.

Practice Projects

To solidify your understanding, consider building these practice projects:

  1. A personal blog with markdown support

  2. A portfolio website showcasing your projects

  3. A simple e-commerce site with product listings and a shopping cart

  4. A dashboard application with data visualization

Remember, the best way to learn is by doing. Don't be afraid to experiment, make mistakes, and ask for help when you need it. The Next.js community is vibrant and supportive, and there's always something new to learn.

As you continue your journey with Next.js 14, you'll discover its power in building fast, scalable, and user-friendly web applications. Keep exploring, keep building, and most importantly, enjoy the process of creating with Next.js!

Did you find this article valuable?

Support Richard Westmoreland by becoming a sponsor. Any amount is appreciated!