Component Files, Imports, and 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:
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:
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:
import LessonCard from "./LessonCard.jsx";And use it exactly the same way as before:
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.
App.jsx imports ./LessonCard.jsx
-> LessonCard.jsx provides its default export
-> App.jsx gets a local LessonCard binding
-> <LessonCard /> uses that functionOne 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:
<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:
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:
import Card from "./LessonCard.jsx";And then:
<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:
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:
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:
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:
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:
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:
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:
LessonCard.jsx
default export LessonCardThen you import it with:
import LessonCard from "./LessonCard.jsx";If a module intentionally exposes a few related values, named exports can make more sense:
chapter-ui.jsx
named export ChapterHeading
named export LessonProgressAnd then:
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:
export const lessonLimit = 6;
export default function LessonList() {
// ...
}Then import both:
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:
src/
App.jsx
components/
LessonCard.jsxNow App.jsx imports it with:
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:
src/
App.jsx
lesson-data.js
components/
LessonCard.jsxFrom LessonCard.jsx, you'd write:
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:
import LessonCard from "./components/LessonCard.jsx";Vite can resolve some imports without you writing the extension, so you'll also see:
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:
LessonCard.jsxThen write:
import LessonCard from "./LessonCard.jsx";with the same casing.
Don't write this:
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:
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:
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:
console.log("LessonCard module evaluated");is top-level module code. It runs when the module is evaluated.
But this:
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:
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:
// lesson-data.js
export let currentLesson = 1;
export function advanceLesson() {
currentLesson += 1;
}Another module imports both:
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:
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:
App.jsx imports LessonCard.jsx
LessonCard.jsx imports App.jsxThat'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:
App.jsx -> LessonCard.jsx
LessonCard.jsx -> App.jsxyou could have:
App.jsx -> LessonCard.jsx
App.jsx -> lesson-data.js
LessonCard.jsx -> lesson-data.jsNow 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:
src/
App.jsx
components/
ChapterHeading.jsx
LessonCard.jsx
LessonList.jsx
lesson-data.js
main.jsxmain.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:
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:
export function LessonCard() {}and then accidentally import it this way:
import LessonCard from "./LessonCard.jsx";Those don't match.
The export is named, so the import needs braces:
import { LessonCard } from "./LessonCard.jsx";Now if the file instead has:
export default function LessonCard() {}then import without braces:
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:
export default function ChapterHeading() {
return <h1>Components Before Abstractions</h1>;
}And LessonProgress.jsx:
export default function LessonProgress() {
return <p>2 of 6 lessons complete</p>;
}Then import them into App.jsx:
import ChapterHeading from "./components/ChapterHeading.jsx";
import LessonProgress from "./components/LessonProgress.jsx";And the component becomes:
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:
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.