Delightful React file/directory structure
joshwcomeau.com
joshwcomeau.com
src/
components/
Button/
Button.tsx
Is just copied ad-hoc for tests, so we get this: test/
components/
Button/
Button.test.tsx
Ideally it would be structured as so: src/
components/
Button/
Button.tsx
Button.test.tsx
This pattern makes it extremely taxing to utilize codemods in the future to transform repos into something else. It also, in my opinion, instills thinking that testing is separate from feature development and is often treated as such. As a result, my org often forgoes proper testing or tacking it on at the very end to just fulfill the motions of the dev cycle.The oddest part is that some staff and principle engineers are very adamant about this structure. It's just additional boilerplate that doesn't help and makes it hard to understand what components have tests (especially when you get in the weeds of not having a flat component directory, where child components have nested directories often several layers deep). I have no idea where this pattern permeates but it should be discouraged in most frontend projects.
Co-location of files should absolutely be encouraged, the alternative is just a jumble of directories that make it hard to grok what is actually happening.
IDK, just ranting now at this point.
Colocation of test files is the hands down winner and encourages the writing of tests. When you're making a change to a code file, you probably won't think to scour the codebase for relevant tests. If you see the test file right next to source file in your editor, you probably will.
Kinda curious to learn how they enforced directory structures. I know now they co-locate tests, but IIRC CRA always used jest and with jest you just set the globs you want to use in the config file. Hardly hardcoded or strictly enforced, but I could be wrong.
The default templates are just a single folder, with tests located adjacent to their source files.
CRA configures jest to look for test files in arc/, but let's users override that.
https://github.com/facebook/create-react-app/tree/main/packa...
https://github.com/facebook/create-react-app/tree/main/packa...
1. Sometimes you want to do some experimental refactor in your application. This might break a lot of tests (cause them to not compile). But you first want to play around with the new change before committing to it and updating all your tests. If your test code is in the same project then the compiler errors will prevent you from doing this.
2. Your test code and application code usually need different dependencies. You don't want your test code to accidentally call functions from some helper library, and you don't want your application code accidentally calling functions from some test framework. If you have a shared list of dependencies and a large team then this will inevitably happen.
3. You don't want your application code to accidentally call helper functions from your test files. If you mix them in the same project then with a large team this will inevitably happen.
4. For code navigation and things like IDE "find usages", it is better to not be flooded with results from test code, in order to be able to focus on discovering how the code works. (Sometimes you do want to be taken to the test code which is why good IDEs allow you to choose to toggle on/off cross-project navigation).
5. Bonus: Sometimes you may want to write your test code in a different programming language then your application. This is uncommon but does happen. For example a C library with a test suite written in C++. Or a webapp backend with tests written in a scripting language using selenium. In these cases you have to have a separate project, and so for consistency you do it as well also for tests that use the same programming language.
I think the last point is actually the most important: it helps formulate the understanding that your test suite should be viewed as its own separate and independent program, not inherently tied to the library code that you are developing. This leads to two insights: 1) there's no reason why you couldn't have more than one test suite to test your library (possibly developed by different teams). 2) more interestingly: you should be able to take your test suite, and run it against a different implementation of your library. This makes sense for something like a test suite for a filesystem or SQL database. But even for your custom library, if you ever need to do a rewrite, or port to a different platform/language, then being able to take your existing test suite with you will be invaluable.
Some people also like to go further the bad way and group code by type rather than feature.
src/
components/
componentA.tsx
componentB.tsx
hooks/
hookA.ts
hookB.ts
tests/
componentA.test.tsx
componentB.test.tsx
Angular pre 1.5 liked this a lot. Must be a Java thing.Another one I despise is the page object pattern for E2E tests. Basically introducing a pseudo-DSL tied to something that is ever changing. I like the Cypress approach of using app actions instead:
https://www.cypress.io/blog/2019/01/03/stop-using-page-objec...
I utilize this in my playwright tests since I found it very useful when writing cypress tests previously.
That said, I'm not 100% sold on the value of testing React components, outside of some very specific cases (eg. a very complex component with lots of internal state). I prefer to write integration / e2e tests that check views/pages/flows as a whole. And so for those, since they aren't connected to specific components, I do have a separate `/tests` folder.
Checkout talk about Mouseless Driven Development by Hadi Hariri[1]
One thing I am reluctant about is having a project dictated by your IDE/editor. I think this is an antipattern. Not everyone uses JetBrains products, I myself use vim. It just makes me sad that some people prefer to have their linting, testing, and other automated checks handled by their IDE rather than using proper git hooks or scripting in general.
I'm mostly a web developer so maybe other types of projects this makes sense.
Will check out the video, always happy to be exposed to new ways of doing things!
Yes, you are right
> It just makes me sad that some people prefer to have their linting, testing, and other automated checks handled by their IDE rather than using proper git hooks or scripting in general.
I agree with you things like linting, testing should check in CI.
there is actually benefit of co-locating whether you use JetBrains IDE or not like easier to explore the code base when you diving into existing project.
A folder for components and a maybe separate one for pages or containers is probably all you need (and even those you could probably stick in components until some structure arises). Have seen quite a lot of code-bases that suffered from people trying to structure their folders around a certain model too early on, change their mind, change it again, result: a mess.
I heavily use the quick file open feature in vscode so file location ultimately doesn't matter. Too often people groom this kind of stuff instead of getting work done.
src/
App/
screens/
Login/
component.tsx
index.tsx
model.tsx
styles.scss
Dashboard/
SomeDashboardPart/
component.tsx
index.tsx
SomeOtherDashboardPart/
component.tsx
index.tsx
helpers/
someHelper.tsx
component.tsx
index.tsx
model.tsx
components/
Button/
component.tsx
index.tsx
`model.tsx` files here are MobX data models used for this specific componentLike:
- Having more than one component in a file. A lot of complex components can be made simpler by breaking then into many smaller, temper components. A lot of these helper components are too specific to warrant generalized use. Adding a new file for each one (especially if they're only 3-5 lines of code) clutters the code base, so keeping them in the same time where they're used makes sense. You can always pull them into a separate file later.
- Not making everything "index.js". That really makes it more difficult to keep track of what's what code is where.
Dislike:
- using indeed.js for experts. This makes it easier to accidentally add circular imports, and much harder to track them down.
- putting all components in a components/ directory. This is fine for smaller apps, but it starts to get unweildy once you add more features, teams, or team members to the repo. This is especially true if you use redux, Apollo, and React-router (or any of their competing libraries). Selectors, reducers, GraphQL queries, get spread across the code base, and it becomes difficult to teach down which component rely on what selectors (for example). This starts to bog down onboarding and cross team collaboration since ownership becomes more difficult to define. You end up with spaghetti code pretty easily.
In my experience, writing a game in mostly React, separating things by feature is more intuitive down the line. Mostly because after some time has passed, finding something is pretty easy and I don't get completely lost in code. And yes, this means having custom hooks in the same file as the actual component sometimes.
I also purposefully let convoluted import statements with lots of '../../../' just exist because my IDE (VS Code) takes care of it and if you really think about it, you never really spend too much time on them.
Then again, I've come to the realization that different things work for different folks. And different projects. If anything, I just encourage more people to try different structures until something 'clicks' with you.
components
Button.tsx
Menu
index.tsx <-- menu component
Link.tsx <-- "Link" component only used within Menu
That way if I want to break out the Button component into multiple sub-components, I don't need to change the imports, but I also don't need to add a re-exporting index.ts in every single folder (it gets annoying once you're up to dozens of folders).Also, for funsies I have both a components directory for reusable components and a separate "app" directory for the core containers that make up the first-class functionality. index.tsx in that folder is what would normally be the App.tsx, so the JSX nesting structure of the app is mirrored in the directory structure:
app
index.tsx <-- entrypoint
Login.tsx
Register.tsx
routes
index.tsx <-- core router
Home.tsx
etc... This also makes SSR easy because then in the root 'src' directory that contains all this I can have a server.tsx with ReactDOM.render and a client.tsx with ReactDOM.hydrate, and each can use their respective isomorphic context providers.Also I don't know if this is standard nowadays but aliasing "src" to the source root and adding it as a baseDir in tsconfig.json means you can use absolute imports.
My approach is similar to his, but is simplified to have one index at the top of the components folder for all the components:
components
index.tsx <- wrapper exports all components
mywidget.tsx
myotherwidget.tsx
mycomplexwidget
mycomplexwidget.tsx
subpart.tsx
Then import is easy, and feels less redundant import { mywidget, mycomplexwidget } from '@foo/components'I think I just don't really like using re-exporting index files if I can avoid them, because I don't see much of a positive cost/benefit in most cases. In your example I'd be doing something like this instead:
import { mywidget } from '@foo/components/mywidget';
import { mycomplexwidget } from '@foo/components/mycomplexwidget';
I could fish for pseudo-objective arguments for this but honestly I think it's mostly just personal preference!Any source on that. I'm in vscode right now with several index.tsx files open and no disambiguation in the tabs.
it's controlled by the "workbench.editor.labelFormat" setting, but the default will disambiguate when two files share a name.
I found this super surprising. I much prefer doing things by feature than by function. Where it goes is a function of which page/view/route it's on. If it's a general purpose component that is used on multiple pages (like `Button`) then, sure, it goes in `src/components`. Otherwise, it goes in `src/routes/app-section/components`. Truth be told, I've taken to doing a setup like this:
src/
- routes/
- app-section/
- effects/
- some-action.effect.ts
- ui/
- app-section.component.tsx
- index.ts
- some-component.component.tsx
- app-section.route.ts
- app-section.types.ts
- index.ts
`effects` basically holds your business logic, `ui` contains section specific components and exports the top-level component via `ui/index.ts`, `thing.route` hooks up the route to state management, `index.ts` provides a bundle for hooking up the effects to your effect system and the route component itself. The `name.type.extension` naming scheme clears up the tab name confusion problem. Maybe I should write my own article about this ;)"Clean," "well-organized," "atomic" ... all adjectives, all descriptive, zero emotion
I think it is kind of weird to ask folks not to use feelings-based words when the point here is that they are describing their opinions.
When Josh writes
> Well, there is no one “right” way, but I've tried lots of different approaches in the 7+ years I've been using React, and I've iterated my way to a solution I'm really happy with.
how would you prefer he have phrased it? The iteration has ended in joy for him. Should he not express his joy? Sure, he could express in a technically-phrased way why it causes him joy (maybe tighter iteration loops, maybe easier generation of value, etc. etc.) but then he'd have to prove those things, wouldn't he?
It's hard enough to just get your opinions out there without also having to prove each one as if you were declaring an ultimate statement of fact.
The other thing is that while "delightful" might be as such to him, I have personally found that my opinions simply do not align with people who speak like this. Using that sort of vocabulary is likely to get you written off as a moron among some folks
Technical writing should be simple and plain.
And yes I am judging entirely by the title.
src/
components/
Post.jsx
Post/
PostHeader.jsx
PostActions.jsx
Then import as: import Post from “components/Post.js”
And within the component: import PostActions from “components/Post/PostAction.js”
I’ve felt this approach eliminates most index.js reexports and aligns closer with a browser’s native import syntax. This all comes at the cost of less encapsulation, but in a private codebase, it’s less of an issue.If you import { foo } you actually import everything. Check your bundle.
I prefer the feature based approach because it is easier to navigate.
Consistent naming: since I named the exported thing, that's what the consumers will call it as well unless they go out of their way to do `import { X as Y } from '...'`. This is useful because it helps ensure all consuming code looks similar at least in it's usage of modules. More familiar code is easier to read and reason about. It's also useful for looking up usages of something. I know I can run $IDE's version of "Find Usage" but sometimes it's easier to just Ctrl+Shift+F > X.
Easier importing: If I have something exported as X then I go to a module that isn't using it and I type X, my editor will suggest I import it from the appropriate module. This _can_ work on default exports but only if you use the same name as was used internally at the point of definition. That kind of defeats the benefit of default imports where you can use whatever name you want without hassle and it's your responsibility to make sure you match the names correctly.
Encourages importing what you need: when you default to named exports, you default to pulling in the bare minimum to do the job. Consider the opposite case. Someone imports some monolithic chunk of code as a single object then does `library.thingIWant` with a bunch of different things. Now you've got a larger possible space to look at when trying to load all of the context of a file in to your head. This is also useful for tree shaking. Assuming you've written your code in a well-defined manner and are using a smart build system, it can more easily eliminate dead code because it knows you never import certain pieces of a module. This applies to both your code and code from third-parties.
Both default and named imports: This is common when working with React. You'll see `import React, { useState } from 'react'` or similar. I don't have a rational answer for this but it rubs me the wrong way.
There are advantages with default exports, e.g. you can name it at usage site the way you want without the `:` syntax. Also some things like `React.lazy` only works with default exports.