[HN Gopher] Delightful React file/directory structure
___________________________________________________________________
Delightful React file/directory structure
Author : joshwcomeau
Score : 82 points
Date : 2022-03-15 13:11 UTC (9 hours ago)
(HTM) web link (www.joshwcomeau.com)
(TXT) w3m dump (www.joshwcomeau.com)
| smotched wrote:
| How are you adding the slight bounce animation on your file
| explorer? its pretty neat.
| ihuman wrote:
| The file exporer's source code is at the bottom of the page. It
| looks like it's animated using framer-motion.
| neurotrace wrote:
| > 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: 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
| ;)
| bayesian_horse wrote:
| 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.
| azemetre wrote:
| 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: 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.
| joshwcomeau wrote:
| 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.
| notapenny wrote:
| 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.
| azemetre wrote:
| 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.
| notapenny wrote:
| 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.
| throwaway858 wrote:
| 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.
| d357r0y3r wrote:
| 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.
| zelphirkalt wrote:
| 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.
| Jernik wrote:
| 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
| azemetre wrote:
| 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.
| gherkinnn wrote:
| That's exactly what Deno does [0] in its standard library.
| Colocate tests with code. It just works.
|
| 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.
|
| https://deno.land/std@0.129.0/collections
| striking wrote:
| Workplace puts tests in a __tests__ folder next to the thing
| under test (e.g. `Button/Button.tsx` is tested by
| `Button/__tests__/Button.test.tsx`).
| mind-blight wrote:
| 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.
| throwaway284534 wrote:
| 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:
| 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.
| keb_ wrote:
| 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.
| efrafa wrote:
| I would avoid default exports at all cost.
| shmde wrote:
| All the tutorials which I have been following always default
| export it. Whats the reason to avoid default export ?
| neurotrace wrote:
| 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.
| wjmao88 wrote:
| 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.
| lawwantsin17 wrote:
| zheksoon wrote:
| I prefer using fractal-style component structure, avoiding
| folders like `components` for single-use components. It looks
| like this: 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 component
| kakuri wrote:
| 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.
| lazrgatr wrote:
| Unrelated to the post, but the site has the creepiest pop-up I've
| ever seen. It scared me a bit.
| tomc1985 wrote:
| 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
| politelemon wrote:
| 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.
| tshaddox wrote:
| Are there useful technical qualities that don't reduce to
| feelings?
| striking wrote:
| "Clean" is used in this blog post.
|
| 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.
| shanehoban wrote:
| 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!
| beaconstudios wrote:
| 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:
| 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.
| dsmmcken wrote:
| I think his criticism of that approach is that you end up with
| a bunch of index.tsx tabs open in your IDE and it can be hard
| to tell what is what.
|
| 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'
| beaconstudios wrote:
| 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!
| deanc wrote:
| > 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.
| beaconstudios wrote:
| here's a screenshot from my VS Code right now:
| https://imgur.com/a/kTbBjJP
|
| it's controlled by the "workbench.editor.labelFormat"
| setting, but the default will disambiguate when two files
| share a name.
| tomphoolery wrote:
| recursive wrote:
| Thanks.
| ldd wrote:
| > 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.
| notapenny wrote:
| 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.
___________________________________________________________________
(page generated 2022-03-15 23:01 UTC)