12 things you didn't know you could do with Nextjs

Anmol Baranwal - Feb 20 - - Dev Community

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.

๐Ÿš€ Visit Optimized Template

template

Please star it so that others can discover it :)

ย 

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.

course

You can view the course.


2. Handling 404 Errors

This is how you can use it with the file structure.

not-found

ย 

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();
  }

  // ...
}
Enter fullscreen mode Exit fullscreen mode

ย 

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Accessing a not-handled case will trigger a 404 error and show the above contents.

not found

ย 

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.

not found

This is truly awesome!

ย 

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Trigger Event:
    When an event that should be debounced (like a keystroke in the search box) occurs, a timer starts.

  2. Wait:
    If a new event occurs before the timer expires, the timer is reset.

  3. 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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

layout

You can even make a nested layout or even separate different parts of the application with separate layouts.

nested layout

The root layout app/layout.js would wrap the dashboard layout app/dashboard/layout.js.

effect

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}</>
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

template

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>
Enter fullscreen mode Exit fullscreen mode

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.');
  }
}
Enter fullscreen mode Exit fullscreen mode

The output in the terminal is.

Fetching revenue data...
Data fetch completed after 3 seconds.
Enter fullscreen mode Exit fullscreen mode

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

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:

  1. At the page level, with the loading.tsx file.
  2. For specific components, with <Suspense>.
<Suspense fallback={<RevenueChartSkeleton />}>
   <RevenueChart />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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 } })
Enter fullscreen mode Exit fullscreen mode
  • 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;
Enter fullscreen mode Exit fullscreen mode

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')
}
Enter fullscreen mode Exit fullscreen mode

This is how On-Demand Revalidation works.

On-Demand Revalidation

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'
Enter fullscreen mode Exit fullscreen mode

For instance, in tsconfig.json.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/components/*": ["components/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Each of the "paths" is relative to the baseUrl location.

// tsconfig.json or jsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src/",
    "paths": {
      "@/styles/*": ["styles/*"],
      "@/components/*": ["components/*"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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! ๐Ÿš€

Anmol-Baranwal (Anmol Baranwal) ยท GitHub

Top 5% GitHub ๐Ÿ’œ Open Source Advocate & Maintainer ๐Ÿ˜„ Full-Stack Developer ๐Ÿ”– Technical Writer (400k+ Reads) ๐Ÿ”ฅ Managing 5+ Communities ๐Ÿš€ Open for Collab - Anmol-Baranwal

favicon github.com

Write more, inspire more.

Ending GIF waving goodbye

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .