Get eBook BundleVolume I index
Components Before Abstractions

Component Files, Imports, and Exports

Ishtmeet Singh @ishtms/July 20, 2026/14 min read
#react#components#modules#imports#exports

Till now we've been keeping our components inside App.jsx, and for a small app that's completely fine. Actually I prefer doing that in the beginning because creating five files for five tiny functions just makes you keep jumping around the editor for no good reason.

But after some time, components start getting their own props, some logic, maybe their own styles, and then App.jsx starts becoming a bit annoying to read. You open the file because you want to understand the page and instead you're scrolling through 200 lines trying to find where one component ends and another starts.

That's usually when splitting components into separate files starts making sense.

And nothing special happens to the React component when you move it. This whole file import/export thing is normal JavaScript module stuff. You export a function from one file, import that function into another file, and React receives the same function it would've received if both functions were sitting in App.jsx.

So let's move one.

Start With Two Components in One File

Suppose App.jsx currently looks like this:

jsx
function LessonCard() {
  return <article>Component Files</article>;
}

export default function App() {
  return <LessonCard />;
}

There's no import for LessonCard because both functions are in the same module. App can directly access the LessonCard binding that's already declared in that file.

And these two functions are already doing different jobs. App describes the page-level UI, while LessonCard describes one lesson card. So moving LessonCard into its own file is pretty reasonable now.

Don't take this as "every component needs its own file", though. If you've got some tiny helper component that's only used by its parent and both are still easy to read together, just leave them together. We're splitting files because it makes the code easier to follow, not because JSX somehow demands a new file.

Move the Component Without Changing the Component

Create src/LessonCard.jsx and move the function there:

jsx
export default function LessonCard() {
  return <article>Component Files</article>;
}

The component itself didn't change. We only added export default because now another module needs access to this function.

Then inside App.jsx:

jsx
import LessonCard from "./LessonCard.jsx";

And use it exactly the same way as before:

jsx
export default function App() {
  return <LessonCard />;
}

That's really all that's happening.

LessonCard.jsx exports a function. App.jsx imports that exported value and gives it the local name LessonCard. Then JSX sees <LessonCard /> and React gets that function as the component type.

text
App.jsx imports ./LessonCard.jsx
  -> LessonCard.jsx provides its default export
  -> App.jsx gets a local LessonCard binding
  -> <LessonCard /> uses that function

One thing I want to make very clear here because beginners sometimes assume React does some project scanning magic: React does not search your folders for a component called LessonCard.

If you write:

jsx
<LessonCard />

then a LessonCard variable needs to exist in that module's scope. Maybe you declared it in the same file, maybe you imported it, doesn't matter. JavaScript needs to know what LessonCard refers to.

Default Exports Can Be Renamed While Importing

We used a default export here:

jsx
export default function LessonCard() {
  return <article>Component Files</article>;
}

A module can have one default export, and the importing file gets to choose whatever local name it wants.

So this is valid:

jsx
import Card from "./LessonCard.jsx";

And then:

jsx
<Card />

Card still refers to the default export from LessonCard.jsx.

Would I do this? Usually no.

If the file exports LessonCard, calling it Card in one file, Lesson in another, and SomeCard somewhere else is just gonna make searching the code more annoying. So normally I'll keep the same name:

jsx
import LessonCard from "./LessonCard.jsx";

But JavaScript doesn't force that for default imports.

Also, only one default export is allowed from a module. This is invalid:

jsx
export default function LessonCard() {}
export default function ChapterCard() {}

You can't have two values both claiming to be the default export. JavaScript will reject the module.

Named Exports Work a Little Differently

Now say one file exports a few related things:

jsx
export function ChapterHeading() {
  return <h1>Components Before Abstractions</h1>;
}

export function LessonProgress() {
  return <p>2 of 6 lessons complete</p>;
}

These are named exports. A module can have multiple of them.

To import them, we use braces:

jsx
import {
  ChapterHeading,
  LessonProgress,
} from "./chapter-ui.jsx";

And unlike a default import, those names need to match what the other file exported.

So this won't work:

jsx
import { Progress } from "./chapter-ui.jsx";

Why? Because chapter-ui.jsx exported LessonProgress. There is no named export called Progress.

If you actually want the local name to be Progress, you can alias it:

jsx
import {
  LessonProgress as Progress,
} from "./chapter-ui.jsx";

Now the exporting file still calls it LessonProgress, but inside this particular module your local binding is named Progress.

And those braces are just ES module syntax, by the way. They're not creating an object and they have nothing to do with the braces we use for JavaScript expressions inside JSX.

Same characters, different syntax.

So Should You Use Default or Named Exports?

You're going to find people with surprisingly strong opinions about this one. For our app, we don't need to turn it into some grand rule.

If a file mainly exists for one component, a default export reads quite nicely:

text
LessonCard.jsx
  default export LessonCard

Then you import it with:

jsx
import LessonCard from "./LessonCard.jsx";

If a module intentionally exposes a few related values, named exports can make more sense:

text
chapter-ui.jsx
  named export ChapterHeading
  named export LessonProgress

And then:

jsx
import {
  ChapterHeading,
  LessonProgress,
} from "./chapter-ui.jsx";

React does not care which export syntax you picked. Once the imported binding refers to the component function, React renders it normally.

You can also mix default and named exports in the same module:

jsx
export const lessonLimit = 6;

export default function LessonList() {
  // ...
}

Then import both:

jsx
import LessonList, {
  lessonLimit,
} from "./LessonList.jsx";

Totally valid JavaScript.

For these first few components though, I normally keep component files focused on their UI. If some data constant starts getting used by multiple components, I'll probably move that into a separate data module instead of slowly filling the component file with unrelated exports.

Relative Import Paths Start From the Current File

Let's move LessonCard.jsx into a components directory:

text
src/
  App.jsx
  components/
    LessonCard.jsx

Now App.jsx imports it with:

jsx
import LessonCard from "./components/LessonCard.jsx";

The ./ means start from the directory containing the file doing the import.

Now suppose LessonCard.jsx needs lesson-data.js, which sits back in src:

text
src/
  App.jsx
  lesson-data.js
  components/
    LessonCard.jsx

From LessonCard.jsx, you'd write:

jsx
import { lessons } from "../lesson-data.js";

.. means go up one directory from the importing file's directory.

This has nothing to do with whichever folder your terminal happens to be sitting in when you run npm run dev. Source module paths are resolved relative to the importing module.

I also prefer keeping the file extension in these local imports:

jsx
import LessonCard from "./components/LessonCard.jsx";

Vite can resolve some imports without you writing the extension, so you'll also see:

jsx
import LessonCard from "./components/LessonCard";

Both can work in a Vite project, but writing the full filename makes the source connection obvious and is closer to native ES module usage.

Filename Case Can Bite You Later

Say the actual file is:

text
LessonCard.jsx

Then write:

jsx
import LessonCard from "./LessonCard.jsx";

with the same casing.

Don't write this:

jsx
import LessonCard from "./lessoncard.jsx";

Some operating systems use case-insensitive filesystems by default, so you might accidentally get away with it locally.

Then the project gets built on a case-sensitive Linux machine and suddenly the module cannot be found.

Very fun.

Git can also make case-only renames a little irritating depending on your filesystem. So just match the stored filename exactly. If your component is LessonCard, then LessonCard.jsx is also a pretty natural filename anyway.

Importing a File Also Means That Module Gets Evaluated

There's another thing going on with imports which is easy to miss when all you're doing is moving components around.

Suppose App.jsx has:

jsx
import LessonCard from "./LessonCard.jsx";

console.log("App module evaluated");

Before App.jsx can continue evaluating normally, its static dependencies need to be loaded and linked.

So if LessonCard.jsx contains invalid syntax, App.jsx doesn't just shrug and continue without the component. Loading that module graph fails.

Now put logs in both places:

jsx
console.log("LessonCard module evaluated");

export default function LessonCard() {
  console.log("LessonCard rendered");
  return <article>One lesson</article>;
}

Those two logs happen for completely different reasons.

This:

js
console.log("LessonCard module evaluated");

is top-level module code. It runs when the module is evaluated.

But this:

js
console.log("LessonCard rendered");

is inside the component function, so it runs whenever React calls that component while rendering.

Those timings are not the same at all. The module itself normally gets evaluated once for that loaded module instance, while the component function can run again and again.

Try the logs once if you want to see it, then remove them because otherwise your console becomes useless very quickly.

And don't start putting app work at module top level just because you want something to "run once".

For example:

jsx
localStorage.setItem("visited", "true");

sitting directly at the top of a module is usually not a good place for that operation. That file could potentially be evaluated somewhere without localStorage, such as server-side code, and you've also attached a browser write to module loading for no real reason.

Do the write from the interaction or synchronization code that actually needs it.

Imported Bindings Stay Connected to Their Export

ES module imports are live bindings.

Let's use plain JavaScript for this:

js
// lesson-data.js
export let currentLesson = 1;

export function advanceLesson() {
  currentLesson += 1;
}

Another module imports both:

js
import {
  advanceLesson,
  currentLesson,
} from "./lesson-data.js";

If advanceLesson() changes the exported currentLesson binding, code in the importer that later reads currentLesson sees the updated value.

The import isn't some separate writable copy created when the file first loaded.

But the importing file also can't do this:

js
currentLesson = 5;

Imported bindings cannot be assigned to by the importer.

Now before you get ideas: this is not a replacement for React state.

If you change an exported variable, React doesn't automatically know you want the screen rendered again. There's no React state setter involved there, so no render gets requested just because some module variable changed.

For changing application UI data, use React state or whatever state system your app is using.

Circular Imports Get Confusing Pretty Fast

You can end up with modules importing each other:

text
App.jsx imports LessonCard.jsx
LessonCard.jsx imports App.jsx

That's a circular import.

ES modules do support cycles, so seeing a cycle does not automatically mean JavaScript refuses to run. But now initialization order becomes harder to understand, and values can be unavailable at points where you expected them to exist.

For React component files, this also often means your dependencies are going in a strange direction. A lower component is importing its page-level parent just to get access to some shared value.

Usually the cleaner fix is to move that shared thing into another module.

Instead of:

text
App.jsx -> LessonCard.jsx
LessonCard.jsx -> App.jsx

you could have:

text
App.jsx -> LessonCard.jsx
App.jsx -> lesson-data.js
LessonCard.jsx -> lesson-data.js

Now both modules can import the data they need without importing each other.

Don't "fix" the cycle by throwing everything into one giant global object either. If two files need the same value, first figure out what that value actually belongs to and put it in a sensible module.

Keep Imports Easy to Follow

For the first app, a folder setup this small is enough:

text
src/
  App.jsx
  components/
    ChapterHeading.jsx
    LessonCard.jsx
    LessonList.jsx
  lesson-data.js
  main.jsx

main.jsx imports App.

App.jsx imports the components used to build the page.

LessonList.jsx might import LessonCard.jsx.

And components can either receive their data through props or import some static data when that makes sense for the app.

You don't need a huge folder system at this stage. Please don't create twelve directories because somebody's production React repo had twelve directories.

If you've got four components, having four files inside components/ is fine.

Later, when the application has actual features with their own UI, data code, tests, and other files, you can start grouping by feature. We can deal with that when there's actually enough code to group.

One Component File Can Still Contain More Than One Component

Splitting LessonCard into another file does not mean every function returning JSX now needs its own .jsx file.

This is completely fine:

jsx
function LessonCount({ count }) {
  return <span>{count} lessons</span>;
}

export default function LessonList() {
  return (
    <section>
      <LessonCount count={6} />
    </section>
  );
}

LessonCount is only used by LessonList, it's tiny, and keeping it right there can make the file easier to understand.

There's also no reason to export it when no other module needs it.

If later another component needs LessonCount, or it starts getting enough logic that having its own file would make things easier to read, move it then.

File splitting doesn't need to be decided from line counts. A 100-line file can be perfectly readable, while a 30-line file can already contain two unrelated jobs.

When Import and Export Syntax Doesn't Match

You're definitely going to hit this error at some point.

You have:

jsx
export function LessonCard() {}

and then accidentally import it this way:

jsx
import LessonCard from "./LessonCard.jsx";

Those don't match.

The export is named, so the import needs braces:

jsx
import { LessonCard } from "./LessonCard.jsx";

Now if the file instead has:

jsx
export default function LessonCard() {}

then import without braces:

jsx
import LessonCard from "./LessonCard.jsx";

This is one of those errors where randomly adding and removing braces until Vite stops complaining is tempting.

Don't.

Open the file you're importing from and look at the export. Is it export default? Use a default import. Is it export function, export const, or another named export? Import that name inside braces.

Much faster than guessing.

Finish Splitting the Page

Let's create ChapterHeading.jsx:

jsx
export default function ChapterHeading() {
  return <h1>Components Before Abstractions</h1>;
}

And LessonProgress.jsx:

jsx
export default function LessonProgress() {
  return <p>2 of 6 lessons complete</p>;
}

Then import them into App.jsx:

jsx
import ChapterHeading from "./components/ChapterHeading.jsx";
import LessonProgress from "./components/LessonProgress.jsx";

And the component becomes:

jsx
export default function App() {
  return (
    <main>
      <ChapterHeading />
      <LessonProgress />
    </main>
  );
}

Now save the app and look at the browser.

Visually, nothing should've changed.

That's kind of the point.

We changed how the JavaScript source is organised, not what our components return. ChapterHeading and LessonProgress moved into their own modules, App.jsx imports them, and React eventually receives the same UI descriptions.

So when you're reading an import such as:

jsx
import LessonCard from "./components/LessonCard.jsx";

you should be able to answer a few things without guessing. Which file exports the value? Is that export default or named? What local name does this module give it? And where is that binding first used?

Once imports start feeling this boring, good. They're supposed to be boring.

Export makes a value available from a module, import gives another module access to that exported value, and JSX can then use the imported component function through its local binding.

React doesn't add another import system on top of JavaScript. We're just using ES modules, then handing those imported component functions to React.