I've made several applications with Next.js and am truly amazed by the amount of things that we can do with it.
I recently went deep into the docs and want to tell everything that I learned. There are so many things that we can do, Next.js is truly awesome :D
I'm writing this for the latest version (v14).
Before continuing, I want to tell you about an optimized template I made for Next.js + TypeScript + Tailwind tech stack.
Β
Most of the examples are from the official docs. So, it's trustworthy.
Let's get started then.
1. Next.js offers a course.
The course is one of the best and easiest ways to get started with Next.js. Unlike other resources, it starts with React and then switches it to Next.js.
It has almost everything from extra resources, and docs and covers the concepts in deep.
Don't worry, I don't write anything without trying it myself.
You can view the course.
2. Handling 404 Errors
This is how you can use it with the file structure.
Β
You can cover the edge cases where the page is not found.
For the below code, it triggers that not-found
page when the route is not handled.
import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
import { updateInvoice } from '@/app/lib/actions';
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) {
const id = params.id;
const [invoice, customers] = await Promise.all([
fetchInvoiceById(id),
fetchCustomers(),
]);
if (!invoice) {
notFound();
}
// ...
}
Β
If you include the below code in not-found.tsx
.
import Link from 'next/link';
import { FaceFrownIcon } from '@heroicons/react/24/outline';
export default function NotFound() {
return (
<main className="flex h-full flex-col items-center justify-center gap-2">
<FaceFrownIcon className="w-10 text-gray-400" />
<h2 className="text-xl font-semibold">404 Not Found</h2>
<p>Could not find the requested invoice.</p>
<Link
href="/dashboard/invoices"
className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-white transition-colors hover:bg-blue-400"
>
Go Back
</Link>
</main>
);
}
Accessing a not-handled case will trigger a 404 error and show the above contents.
Β
I've used it before, and the conventions changed slightly after the latest version update. For instance, I made this screen in my past project.
Β
You can read on the official docs.
3. Plugin to improve Accessibility practices.
Accessibility is truly important, and Google even uses the concept of the Next Billion Users corresponding to that.
This is one of the most useful plugins that I've found so far.
By default, Next.js includes the eslint-plugin-jsx-a11y plugin to help catch accessibility issues early. For example, this plugin warns if you have images without alt text, use the aria-* and role attributes incorrectly, and more.
This will be used with the ESLint command npm run lint
. Make sure it is configured in package.json
.
For instance, if we miss an alt prop with Image
then the below error is shown after using Lint. With Nextjs 14, it is a default so only the aria attributes will be useful with the plugin.
45:25 Warning: Image elements must have an alt prop,
either with meaningful text, or an empty string for decorative images. jsx-a11y/alt-text
You can read more on the npm package.
4. The concept of debouncing.
This is one of the most exciting concepts that I've seen so far.
Trust me, I'm not exaggerating.
Debouncing is a programming practice that limits the rate at which a function can fire.
How debouncing works
Trigger Event:
When an event that should be debounced (like a keystroke in the search box) occurs, a timer starts.Wait:
If a new event occurs before the timer expires, the timer is reset.Execution:
If the timer reaches the end of its countdown, the debounced function is executed.
You can implement debouncing in a few ways, including manually creating your own debounce function. To keep things simple, we'll use a library use-debounce
.
// Importing the useDebouncedCallback
import { useDebouncedCallback } from 'use-debounce';
// Inside the Search Component...
// Creating a debounced search function
const handleSearch = useDebouncedCallback((term) => {
// Logging the search term
console.log(`Searching... ${term}`);
// Creating URL parameters
const params = new URLSearchParams(searchParams);
// Updating the 'query' parameter with the search term
if (term) {
params.set('query', term);
} else {
// Removing the 'query' parameter if the search term is empty
params.delete('query');
}
// Updating the URL with the new search parameters
replace(`${pathname}?${params.toString()}`);
}, 300); // Debouncing the function for 300 milliseconds
This function will only run the code after a specific time once the user has stopped typing (300ms).
So, suppose you have to check validation for a form field, then you can use this so that they don't get error warnings even before they finish typing. So Awesome!
You can read more on the npm package.
5. How to use Image component properly in Next.js.
I found this after a long time, and it helped me a lot.
In the recent versions of Next.js, we import the Image component from legacy.
import Image from 'next/legacy/image'
// Rather than import Image from 'next/image'
It is further optimized and provides an unbelievable number of options.
In this legacy version, it is mandatory to use width
and height
so you can specify fixed dimensions.
import Image from 'next/legacy/image'
export const MyImage = () => {
return (
<Image
src="me.png"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
You can use layout="responsive"
as an attribute so it would be responsive depending upon the width and space of the parent container.
Now, I don't want that. It can get too small so the docs specify one more way to do that.
For instance, if you know your styling will cause an image to be full-width on mobile devices, in a 2-column layout on tablets, and in a 3-column layout on desktop displays, you should include a size property such as the following.
import Image from 'next/legacy/image'
const Example = () => (
<div className="">
<Image
src="/example.png"
layout="fill"
sizes="(max-width: 768px) 100vw,
(max-width: 1200px) 50vw,
33vw"
/>
</div>
)
It's again not that flexible, so the following is the perfect solution since we want to modify the dimensions using the Tailwind.
For this, you need layout="fill"
and the parent container should be relative
. This way, you can manipulate the dimensions of the image using breakpoints sm, md, lg...
in the tailwind.
<div className="relative h-[50px] w-[50px]">
<Image
src="/example.png"
layout="fill"
alt="logo"
/>
</div>
You can also implement the same thing with video as well.
6. Using client components in the layout.
A layout is a UI that is shared between multiple routes. On navigation, layouts preserve state, remain interactive, and do not re-render. Layouts can also be nested.
You can define a layout by default exporting a React component from a layout.js file. The component should accept a children
prop that will be populated with a child layout (if it exists) or a page during rendering.
For instance, this layout will be shared with the /dashboard
and /dashboard/settings
pages.
You can even make a nested layout or even separate different parts of the application with separate layouts.
The root layout app/layout.js
would wrap the dashboard layout app/dashboard/layout.js
.
There is also a concept of route groups which you can read from the official docs. It lets you create different layouts for different parts of the application.
The most surprising thing is that you can even create multiple root layouts. WOW, Next.js!
So, the problem is that we have to wrap something that uses client components, and since metadata doesn't work in client components it is difficult to add everything in that one single layout component.
You must have heard of AOS Animations. So let's see how we can use it for just the single page layout. I'm not covering how to separate it, which you can read in the route groups that I shared above.
You can make a component for aos-wrapper.tsx
'use client'
import { useEffect, type ReactNode } from 'react'
import AOS from 'aos'
import 'aos/dist/aos.css'
export const AosWrapper = ({ children }: { children: ReactNode }) => {
useEffect(() => {
AOS.init({
duration: 800,
once: true,
})
}, [])
return <>{children}</>
}
You can wrap that like below.
import type { Metadata } from 'next'
import '@/styles/globals.css'
import { AosWrapper } from '@/components/aos-wrapper'
import React from 'react'
export const metadata: Metadata = {
title: '',
description: '',
}
export default function LandingPageLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<head />
<body>
<AosWrapper>{children}</AosWrapper>
</body>
</html>
)
}
This way, you can use client components inside any layout.
7. Concept of Templates.
To differentiate from the concept of layouts, templates were created.
Templates are similar to layouts in that they wrap each child layout or page. Unlike layouts that persist across routes and maintain state, templates create a new instance for each of their children on navigation.
This means that when a user navigates between routes that share a template, a new instance of the component is mounted, DOM elements are recreated, the state is not preserved, and effects are re-synchronized.
Templates are preferred over the layout in many cases such as features that rely on useEffect (e.g. logging page views) and useState (e.g. a per-page feedback form).
Templates can modify how certain features work within your framework. For example, they can control the display of fallback UIs in Suspense Boundaries during page transitions, which layouts cannot do.
export default function Template({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
// second part
<Layout>
{/* Note that the template is given a unique key. */}
<Template key={routeParam}>{children}</Template>
</Layout>
In terms of nesting, template.js
is rendered between a layout and its children.
8. What happens if one data request is slower than all the others?
Let's simulate a slow data fetch.
export async function fetchRevenue() {
try {
// We artificially delay a response for demo purposes.
// Don't do this in production :)
console.log('Fetching revenue data...');
await new Promise((resolve) => setTimeout(resolve, 3000));
const data = await sql<Revenue>`SELECT * FROM revenue`;
console.log('Data fetch completed after 3 seconds.');
return data.rows;
} catch (error) {
console.error('Database Error:', error);
throw new Error('Failed to fetch revenue data.');
}
}
The output in the terminal is.
Fetching revenue data...
Data fetch completed after 3 seconds.
You have added an artificial 3-second delay to simulate a slow data fetch. The result is that -> your whole page is blocked while the data is being fetched.
With dynamic rendering, your application is only as fast as your slowest data fetch.
You can solve it using streaming :)
9. Concept of Streaming.
Let's cover it in brief.
Streaming is a data transfer technique that allows you to break down a route into smaller "chunks" and progressively stream them from the server to the client as they become ready.
By streaming, you can prevent slow data requests from blocking your whole page. This allows the user to see and interact with parts of the page without waiting for all the data to load before any UI can be shown to the user.
Streaming works well with React's component model, as each component can be considered a chunk.
There are two ways you implement streaming in Next.js:
- At the page level, with the
loading.tsx
file. - For specific components, with
<Suspense>
.
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
On refreshing the page, we will see the other information almost immediately, while a fallback skeleton is shown for <RevenueChart>
.
So many concepts are involved so use it after reading about it thoroughly.
You can read on the official docs.
10. Disabling scroll position when changing route.
By default, Next.js will scroll to the top of the page when navigating to a new route.
You can disable this behavior by passing scroll: false
to router.push()
or router.replace()
.
For instance, see the example below.
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button
type="button"
onClick={() => router.push('/dashboard', { scroll: false })}
>
Dashboard
</button>
)
}
11. Cache and Revalidate with fetch.
As you're aware, Next.js extends the native Web fetch() API
to allow each request on the server to set its own persistent caching semantics.
With this extension, cache
indicates how a server-side fetch request will interact with the framework's persistent HTTP cache.
export default async function Page() {
// This request should be cached until manually invalidated.
// Similar to `getStaticProps`.
// `force-cache` is the default and can be omitted.
const staticData = await fetch(`https://...`, { cache: 'force-cache' })
// This request should be refetched on every request.
// Similar to `getServerSideProps`.
const dynamicData = await fetch(`https://...`, { cache: 'no-store' })
// This request should be cached with a lifetime of 10 seconds.
// Similar to `getStaticProps` with the `revalidate` option.
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 10 },
})
return <div>...</div>
}
In case of
force-cache
(default).
If there is a match and it is fresh, it will be returned from the cache.
If there is no match or a stale match, Next.js will fetch the resource from the remote server and update the cache with the downloaded resource.
In case of
no-store
.
Next.js fetches the resource from the remote server on every request without looking in the cache, and it will not update the cache with the downloaded resource.
fetch(`https://...`, { next: { revalidate: false | 0 | number } })
-
0
- Prevent the resource from being cached. -
number
(in seconds) - Specify the resource should have a cache lifetime of at most how many seconds.
You can also do an on-demand revalidation.
The concept of revalidateTag
only invalidates the cache when the path is next visited. This means calling revalidateTag
with a dynamic route segment will not immediately trigger many revalidations at once. The invalidation only happens when the path is next visited.
You can use it like below.
revalidateTag(tag: string): void;
You can read the docs that covers it in deep.
Next.js has a cache tagging system for invalidating fetch requests across routes.
In case you're wondering about Tag
.
Tag
is a string representing the cache tag associated with the data you want to revalidate.
You can then revalidate this fetch
call tagged with collection
by calling revalidateTag
in a Server Action.
'use server'
import { revalidateTag } from 'next/cache'
export default async function action() {
revalidateTag('collection')
}
This is how On-Demand Revalidation works.
If you want to read in deep, you can read about on-demand revalidation from official docs.
12. Absolute Imports and Module Path Aliases.
Next.js has in-built support for the "paths"
and "baseUrl"
options of tsconfig.json
and jsconfig.json
files.
These options allow you to alias project directories to absolute paths, making it easier to import modules.
// Before
import { Button } from '../../../components/button'
// after
import { Button } from '@/components/button'
For instance, in tsconfig.json
.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"]
}
}
}
The baseUrl
configuration option allows you to import directly from the root.
Now you can import the module using @/components/...
import Button from '@/components/button'
export default function HomePage() {
return (
<>
<h1>Hello World</h1>
<Button />
</>
)
}
Each of the "paths"
is relative to the baseUrl
location.
// tsconfig.json or jsconfig.json
{
"compilerOptions": {
"baseUrl": "src/",
"paths": {
"@/styles/*": ["styles/*"],
"@/components/*": ["components/*"]
}
}
}
This is how you can use imports after specifying that.
// pages/index.js
import Button from '@/components/button'
import '@/styles/styles.css'
import Helper from 'utils/helper'
Can you tell what concept this represents?
import type { Route } from 'next'
import Link from 'next/link'
function Card<T extends string>({ href }: { href: Route<T> | URL }) {
return (
<Link href={href}>
<div>My Card</div>
</Link>
)
}
Also, let me know in the comments how you can style active links without using CSS in next.js?
I think it will take weeks if we have to study every single thing about Next.js docs. I've pretty much gone in deep. But there is still a lot to learn, and I personally don't think it's feasible.
It's just as simple as searching and using docs as needed.
So, did you like this post? Let me know in the comments.
Which point is most surprising to you?
I write by researching thoroughly and sharing my experiences. You can support me by sponsoring me on GitHub.
Please please follow me on GitHub & Twitter :)
If you are keen on sponsoring this post, shoot me a message at anmolbaranwal119@gmail.com or hit me up on Twitter! π
Write more, inspire more.
Top comments (13)
As I mentioned earlier, the simplest ones are the most important to me. Learning how to use the Image component properly & client components in layouts was the most useful for me.
What new concepts did you discover here?
Nice, you have some great article @anmolbaranwal π
Thanks :D
Thanks for this long read, Anmol!
Just wanted to let you know that you are doing great! ππ
Thanks Arjun! I will try to write even better content :D
Thanks for this article, Anmol! You explained very well. Congrats!
Thanks! Appreciate that you found it good enough :D
All this is documented in the next.js docs. All you need is to read the docs before using the tool to use it correctly.
Yep! I did a little course before v13 was released and after that I've only read docs and searched for whatever I need. Next.js documentation provides everything one needs to build the best application :D
Although, it could be improved a bit for sure.
Wow! Awesome article. π₯
Thanks! Some concepts are brief, while others go deep. Appreciate it :D
Thank you! I read with interest
I will improve the structure next time so it is easier to read.
Thank you for reading :)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.