Kinda wish Josh would mention how he structures his tests in his projects. Something I'm currently struggling at work is that within our code repos, the pattern that everyone seems to copy is mimic the src/ directory for the test/ directory, rather than co-locating tests along with components. This means a structure for components:
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.
I think people do the "separate directory for tests" thing because test runners have, in the past, shipped with a default configuration to target a test directory, rather than match test files by suffix.
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.
When you make a change to a code file, you should run your tests. Why else do you have them? A test failure should result in you going to fix that or, if appropriate, change assumptions of the test. I don't see how it is harder to add tests either.
What about changes that don't cause test failures? A really quick one I can think of is a pure addition. Lets say you have tests for Feature A in 3 places. You add a thing to Feature A, and find 2 places where Feature A is tested, add tests to those, feel like you did your due diligence and move on. Co-located tests would fix this problem
It's not that it's impossible to test, it's just more of "out of sight, out of mind." Couple this with a poor engineering culture in general, it's easier for me to understand how this pattern encourages very poor testing.
I agree! I'd do `Button.test.tsx` right inside the component directory. Managing a mirrored structure feels like a lot of wasted effort. Same for Storybook stories (`Button.story.tsx`).
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.
There are several good reasons to keep your test code in a completely separate directory (or "project")
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.
I do wonder if it's Java as well, I've never been exposed to that ecosystem but it seems many bad (IMO at least) practices seem to originate from Java.
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:
Never seen this talk, will definitely watch it later today.
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!
> 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.
If they're using something like create-react-app, it may force that structure on them. I remember in earlier versions it was harder to configure other folders to put tests in. I prefer your ideal structure as well, if you're going to co-locate files relating to Button, do it properly, makes it much easier to find things and in this case see if there are tests for your component.
hmm. I honestly never used CRA outside of an interview assessment and don't really remember much. Early on in my web career I was encouraged to create my own scaffolding tools, that practice just always stuck with me.
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.
Could also be that I'm remembering it wrong, but I recall having some issues with CRA and structuring tests at some point. In any case, CRA sets up a separate src and test folder, so I guess a lot of people just think they should structure their tests that way.
Yes, I've seen this as well. I will admit I do using a __tests__ or __mocks__ folder to "separate" helper files for testing; but I think now I prefer core tests to be in the component directory entirely.
Keep it as flat as possible until you really, really need to structure it.
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 agree. I'll make a new folder when I see a pattern and I don't hesitate to move things around if I realize something's not working. I find a similar folder structure develops across my apps over time but it really depends on which libraries and the app. Usually a separate folder for high level screens. A components folder. Models and services get their own folder. State management too sometimes depending on complexity. Then the app itself might have some clearly defined sections or features that get foldered up.
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.
I have a bit of a preference for this approach as well, but having worked on numerous large projects I have to agree with Josh's arguments against it. Maybe on a small personal project it can succeed but on large ongoing projects I favor organizing by function rather than feature.
There are some things I really like about this, and some things that can shoot you in the foot.
Like:
- 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.
> Finally, in terms of organization, I want things to be organized by function, not by feature.
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.
I do something similar, except I directly use index.tsx as the widget in a directory structure, like this:
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:
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.
That issue with too many index.ts files used to be a major issue but nowadays I use VS Code which disambiguates matching filenames with their parent directory.
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!
> That issue with too many index.ts files used to be a major issue but nowadays I use VS Code which disambiguates matching filenames with their parent directory.
Any source on that. I'm in vscode right now with several index.tsx files open and no disambiguation in the tabs.
> Finally, in terms of organization, I want things to be organized by function, not by feature.
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:
`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 ;)
Can we please stop describing technical things using feelings-based words? It's foppish, it makes tech sound really really disingenuous and fake, and it cheapens the words as well.
"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.
Then can we at least stick to words with precision, and not the thesaurus drivel popularized by marketing people?
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
Whenever I see the word 'beautiful', I often find that thing is not beautiful. But it gets used, a _lot_. The only explanation I can come up with, is they are simply assuming that you are incapable of forming your own thoughts, and need to be told what to feel.
When it comes to the index.js issue, I’m partial to “unwrapping” component directories. For example, if a Post component has a few one off sub-components, they’re placed in Post subdirectory:
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.
I used to do this as a newb before I learned about how index.js works. In hindsight, it makes a lot of sense (especially when you consider browser parity), and I find it amusing how common the use of index.js is in React codebases, when Ryan Dahl named index.js one of his greatest mistakes when creating Node.
In SPAs it's pretty easy to tell to which "code feature" a component belongs. For one group of component, it depends on what page they are on. For the rest it basically doesn't matter.
I prefer the feature based approach because it is easier to navigate.
I cannot get over the quality of this site, the subtle sounds, the design, the interactions, just everything. Josh is a damn genious, from one FE dev to another, bravo!
If your whole file is semantically the default export, then why not?
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.
Not OP but I avoid default exports unless I need to do a lazy import. For me, there's a few reasons: consistent naming, easier importing, encourages importing what you need, avoids weird situation where you do both a default and named import.
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.
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.