What’s the Difference between Named and Default Exports?


I’m currently working on a simple To-Do app to learn Next.js, following a tutorial by Web Dev Simplified, and I encountered an error.

Failed to compile
./src/app/page.tsx:3:0
Module not found: Can't resolve './components/TodoTask'
  1 | import Link from "next/link"
  2 | import { prisma } from "./db"
> 3 | import TodoTask from "./components/TodoTask"
  4 | 
  5 | /*  how to create a new todo item to test: 
  6 |   await prisma.todo.create({

https://nextjs.org/docs/messages/module-not-found
This error occurred during the build process and can only be dismissed by fixing the error.

One such error, which might seem trivial, but for me, it ended with a TIL and lots of reading about named and default exports.

Importing and exporting are particularly common in JavaScript frameworks like React and its server-side rendering counterpart, Next.js; you’d think I would know this by now, but until today I’d only ever used default exports.

In this blog post, I’ll illustrate this issue with an example and explain how to solve it. Plus, we’ll learn more about named and default exports.

The files:

Here’s my component TodoTask.tsx:

type TodoTaskProps = {
    id: number
    title: string
    complete: boolean
}

export function TodoTask({ id, title, complete }: TodoTaskProps) {
    return (
        <li className="flex gap-1 items-center">
            <input id={id} type="checkbox" className="cursor-pointer peer" />
            <label htmlFor={id} className="peer-checked:line-through">
                {title}
            </label>
        </li>
    )
}  

And here’s page.tsx where I’m importing and using the component:

import Link from "next/link"
import { prisma } from "./db"
import TodoTask from "./components/TodoTask"

function getTodoList() {
  return prisma.todo.findMany()
}


export default async function Home() {
  const todos = await getTodoList()

  return (
    <>
      <header className="flex justify-between mb-4 items-center">
        <h1 className="text-2xl" >To-Do List</h1>
        <Link
          className="border border-slate-200 text-slate-200 px-2 py-2 rounded
     hover:bg-violet-800 hover:text-slate-200 hover:border-slate-200 outline-none"
          href="/new">New</Link>
      </header>
      <ul className="pl-4">
        {todos.map(todo => (
          <TodoTask key={todo.id} {...todo} />
        ))}
      </ul>
    </>
  )
}

The issue:

First, I immediately noticed that I needed to use “../components/TodoTask” instead of “./components/TodoTask” because page.tsx is in the app directory, so I had to go one level up (..) to reach components.

Once I fixed that, I encountered another error:

Unhandled Runtime Error
Error: Unsupported Server Component type: undefined

Now, I was left scratching my head, determined to figure it out without rewatching that part of the video for an easy fix…

I discovered the error was due to the way I was exporting and importing the TodoTask component from TodoTask.tsx.

In my TodoTask.tsx, I’m using a named export:

export function TodoTask({ id, title, complete }: TodoTaskProps) {
    ...
}

But in my page.tsx file, I’m trying to use a default import:

import TodoTask from "./components/TodoTask"

I had never used named exports until today; I had no idea they were a thing.

The Fix:

To fix the issue, I adjusted the import to match the named export:

import TodoTask from "./components/TodoTask"
import { TodoTask } from "../components/TodoTask"

So other than syntax, what are the key differences between named and default exports?

Named Exports

Named exports are used when a module exports multiple items, like functions or objects. Each exported item has a name, and they are wrapped in curly braces {}. Here is an example:

export const add = (x, y) => x + y;
export const subtract = (x, y) => x - y;

And then import them like this:

import { add, subtract } from './utils.js';

console.log(add(2, 3)); // outputs: 5
console.log(subtract(5, 2)); // outputs: 3
Advantages of Named Exports
  • It promotes code clarity and self-documentation by maintaining the name of variables, functions, or classes.
  • You can export multiple values, which can then be imported selectively based on your needs.
  • It allows renaming while importing, which can be helpful when you want to avoid naming collisions.

Default Exports

Default exports are used when a module only exports a single item, like a function or a class. Here is an example:

export default function() {
  console.log('Hello, world!');
};

And then import it like this:

import greet from './greeting.js';

greet(); // outputs: Hello, world!

We can see that the function doesn’t have a name. It’s a default export, and we can name it anything we want when importing.

Advantages of Default Exports
  • It is simpler and quicker to write, especially if your module exports only one thing.
  • It allows renaming without extra syntax. You can import the default export with any name you prefer.

When to Use Default Exports

  1. Single-Component Modules: In React or Next.js applications, it’s common to have one module per component. Since each file exports just one component, using a default export makes sense. For example:
import React from 'react';

const Button = ({ onClick, children }) => (
  <button onClick={onClick}>
    {children}
  </button>
);

export default Button;

Then you can import this component as follows:

import Button from './Button';

// Now Button component can be used in this file
  1. Library Main Module: If you’re creating a library and there’s a primary function or class that you expect users to use most of the time, you might choose to export it as a default export. This makes it a bit easier for users to import and use. For example, the popular library React exports the React object as a default export.

When to Use Named Exports

  1. Utility Libraries: If you’re creating a utility library that provides a set of related functions (like a math library, string manipulation library, or a set of custom React hooks), using named exports would be a good choice. For example:
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

Then, in another file, you can import just the functions you need:

import { add, subtract } from './mathUtils';

console.log(add(2, 3)); // outputs: 5
console.log(subtract(5, 2)); // outputs: 3
  1. Multiple Exports from a Module: In scenarios where you have multiple related items being exported from a single module, named exports can be beneficial. This is often seen in Node.js models, where you might export a model class and related constants. For example:
export const USER_ROLE_ADMIN = 'admin';
export const USER_ROLE_USER = 'user';

export class User {
  constructor(name, role) {
    this.name = name;
    this.role = role;
  }
  // methods...
}

These items can then be imported as needed:

import { User, USER_ROLE_ADMIN } from './User';

let admin = new User('Admin', USER_ROLE_ADMIN);

Key Differences Between Named and Default Exports?

  1. Number of Exports: A module can have multiple named exports but only one default export.
  2. Syntax: Named exports require you to wrap the imported module’s name in curly braces {} unless you use the as keyword, while default exports do not.
  3. Flexibility: Named exports are more flexible, allowing a module to export multiple things. Default exports, however, are less flexible but simpler to use if a module only needs to export one thing.
  4. Renaming: For named exports, if you want to rename the imported item, you have to use the as keyword. For default exports, you can name anything you want during the import.

conclusion…

This is just the tip of the iceberg, but I now have a good understanding of the basics, and if you’re reading this, I hope you learned something as well.

Keep learning, keep exploring, and most importantly, keep coding. The journey is long, but every step and struggle counts. Happy coding!

Extra reading:


Leave a Reply

Your email address will not be published. Required fields are marked *