Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active November 16, 2024 05:59
Show Gist options
  • Save ryanflorence/daafb1e3cb8ad740b346 to your computer and use it in GitHub Desktop.
Save ryanflorence/daafb1e3cb8ad740b346 to your computer and use it in GitHub Desktop.

Folder Structure

Please note

While this gist has been shared and followed for years, I regret not giving more background. It was originally a gist for the engineering org I was in, not a "general suggestion" for any React app.

Typically I avoid folders altogether. Heck, I even avoid new files. If I can build an app with one 2000 line file I will. New files and folders are a pain.

But when you're in a decently large organization where different development teams work within pretty well-defined feature boundaries of an application, I like the following approach but I would keep the feature folders flat, no route nesting.

Also, don't you dare throw this at your eng team and be like "this is the way". Make up your own minds and don't use me as some weird appeal to authority, I'm just an average dev like anybody else.

Motivations

  • Clear feature ownership
  • Module usage predictibility (refactoring, maintainence, you know what's shared, what's not, prevents accidental regressions, avoids huge directories of not-actually-reusable modules, etc)
  • CI runs only the tests that matter (future)
  • Code splitting (future)

How it works

The file structure maps directly to the route hierarchy, which maps directly to the UI hierarchy.

It's inverted from the model that we've used in other systems. If we consider all folders being either a "generic" or a "feature" folder, we only have one "feature" folder but many "generic" folders.

Examples of "feature" folders:

  • Surveys
  • Admin
  • Users
  • Author

Examples of "generic" folders:

  • components
  • helpers
  • stores
  • actions

Given this route config:

var routes = (
  <Route name="App">
    <Route name="Admin">
      <Route name="Users"/>
      <Route name="Reports"/>
    </Route>
    <Route name="Course">
      <Route name="Assignments"/>
    </Route>
  </Route>
);

We would now set up our directories like this:

app
└── screens
    └── App
        └── screens
            ├── Admin
            │   └── screens
            │       ├── Reports
            │       └── Users
            └── Course
                └── screens
                    └── Assignments

Next, each of these screens has an index.js file, which is the file that handles the entry into the screen, also known as a "Route Handler" in react router. Its very much like a Route in Ember. We'll also have some top-level application bootstrapping stuff at the root, like config/routes.js.

app
├── config
│   └── routes.js
├── screens
│   └── App
│       ├── screens
│       │   ├── Admin
│       │   │   ├── screens
│       │   │   │   ├── Reports
│       │   │   │   │   └── index.js
│       │   │   │   └── Users
│       │   │   │       └── index.js
│       │   │   └── index.js
│       │   └── Course
│       │       ├── screens
│       │       │   └── Assignments
│       │       │       └── index.js
│       │       └── index.js
│       └── index.js
└── index.js

With this structure, each screen has its own directory to hold its modules. In other words, we've introduced "scope" into our application file structure.

Each will probably have a components directory.

app
├── config
│   └── routes.js
├── screens
│   └── App
│       ├── components
│       ├── screens
│       │   ├── Admin
│       │   │   ├── components
│       │   │   ├── screens
│       │   │   │   ├── Reports
│       │   │   │   │   ├── components
│       │   │   │   │   └── index.js
│       │   │   │   └── Users
│       │   │   │       ├── components
│       │   │   │       └── index.js
│       │   │   └── index.js
│       │   └── Course
│       │       ├── components
│       │       ├── screens
│       │       │   └── Assignments
│       │       │       ├── components
│       │       │       └── index.js
│       │       └── index.js
│       └── index.js
└── index.js

These components are used only in the current screen, not even the child screens. So what about when you've got some shared components between screens?

Shared Modules

Every screen also has a "shared" generic directory. If its children share any components with each other or the parent, we put the shared code in "shared". Here is our growing app with some new shared, and not shared modules.

app
├── config
│   └── routes.js
├── screens
│   └── App
│       ├── components
│       ├── screens
│       │   ├── Admin
│       │   │   ├── components
│       │   │   ├── screens
│       │   │   │   ├── Reports
│       │   │   │   │   ├── components
│       │   │   │   │   ├── stores
│       │   │   │   │   │   └── ReportsStore.js
│       │   │   │   │   └── index.js
│       │   │   │   └── Users
│       │   │   │       ├── components
│       │   │   │       └── index.js
│       │   │   ├── shared
│       │   │   │   └── stores
│       │   │   │       ├── AccountStore.js
│       │   │   │       └── UserStore.js
│       │   │   └── index.js
│       │   └── Course
│       │       ├── components
│       │       ├── screens
│       │       │   └── Assignments
│       │       │       ├── components
│       │       │       └── index.js
│       │       └── index.js
│       ├── shared
│       │   └── components
│       │       ├── Avatar.js
│       │       └── Icon.js
│       └── index.js
├── shared
│   └── util
│       └── createStore.js
└── index.js

Note Admin/shared; Reports and Users can both access the shared stores. Additionally, every screen in the app can use Avatar.js and Icon.js.

We put shared components in the nearest shared directory possible and move it toward the root as needed.

Shared module resolution

The way modules in CommonJS are resolved is pretty straightforward in practice: its all relative from the current file.

There is one piece of "magic" in the way modules resolve. When you do a non-relative require like require('moment') the resolver will first try to find it in node_modules/moment. If its not there, it will look in ../node_modules/moment, and on up the tree until it finds it.

We've made it so that shared resolves the same way with webpack modulesDirectories. This way you don't have to require('../../../../../../../../../../shared/Avatar') you can simply do require('components/Avatar') no matter where you are.

Tests

Tests live next to the modules they test. Tests for shared/util/createStore.js live in shared/util/__tests__/createStore.test.js.

Now our app has a bunch of __tests__ directories:

app
├── __tests__
├── config
│   └── routes.js
├── screens
│   └── App
│       ├── components
│       │   ├── __tests__
│       │   │   └── AppView.test.js
│       │   └── AppView.js

... etc.

├── shared
│   └── util
│       ├── __tests__
│       │   └── createStore.test.js
│       └── createStore.js
└── index.js

Why "Screens"?

The other option is "views", which has become a lot like "controller". What does it even mean? Screen seems pretty intuitive to me to mean "a specific screen in the app" and not something that is shared. It has the added benefit that there's no such thing as an "MSC" yet, so the word "screen" causes people to ask "what's a screen?" instead of assuming they know what a "view" is supposed to be.

@eddyystop
Copy link

Like @sheltonial, we were happy with this structure, particularly how shared components & utilities, plus actions & reducers can be discovered.

However, like @petrometro commented on Dec 16, 2015, the benefit of the structure rather degraded once we started writing tests. While webpack is fine with the shared module resolution, babel doesn't know how to find the action/reducer/component required by the unit under test.

We've had to go back to using ../../../foo whenever a test ran into a problem.

Can anyone suggest a better way around this?

[Placing our modules under node_modules is a non-starter. We use imports rather than requires.]

@eddyystop
Copy link

The structure works for testing if you Webpack + Babel your tests before running them. I guess that's no surprise. I have to run Babel anyway since my app is in ES6, and running Webpack as well doesn't increase build time much.

My package.json includes:

"scripts": {
    "pretest": "webpack --target node --config ./webpack.config.test.js",
    "test": "mocha ./tmp/test.bundle.js --require ./test/test_helper.js  --recursive"
 },

The Webpack config is straightforward. The test_helper.js referred to above is in ES5.

There is a problem with Chai 3.5.0 as it does not work in strict mode, whereas Babel wraps code in "use strict". I have the following in the Webpack config, but it doesn't solve the problem.

test: /\.(js|jsx)$/, exclude: /node_modules/, loaders: [ 'babel-loader' ]

There is a patch to Chai in the dev branch which allows it to run in strict mode.

/node_modules/chai/lib/chai/assertion.js line 33
from flag(this, 'ssfi', stack || arguments.callee);
to flag(this, 'ssfi', stack || Assertion); 

And the test script worked after this.

@eddyystop
Copy link

eddyystop commented May 1, 2016

Babel can also do shared module resolution using babel-plugin-resolver. Using this you may not have to create a Webpack bundle when running tests on node, thus saving yourself a bundle (sic) of time.

Caveats:
(1) The plugin is only called when you use import not require.
(2) You will still have to run Webpack for tests if Babel loaders need to process your files, e.g. you have import style from './style.css'
(3) You can run into logical problems if you have the same file names in different shared folders.

The Babel config for the sample folder structure would include something like:

{
  "presets": ["es2015", "stage-0", "react"],
  "plugins": [
    ["resolver", { "resolveDirs": [
      "shared",
      "screens/App/shared",
      "screens/App/screens/Admin/shared",
    ]}],
    ...
  ],
  ...
}

Modules are resolved by looking into the folders in the order they are specified in the config. The plugin does not 'walk' the folder structure. Hence you should avoid using the same file names in different shareable folders.

You can alternatively use the NODE_PATH environment variable. This option requires neither Babel nor Webpack. Its an evolutionary leftover from Node's early days, but is still supported.

It works similarly to babel-plugin-resolver but using absolute paths.

@kriegerd
Copy link

kriegerd commented May 3, 2016

Does anyone knows of an example app that uses this structure?
I'm just starting with most of these libs and it's a bit hard to wrap my head around it all. Thanks!

@iammerrick
Copy link

We adopted this for our project and decided to leave the practice for one reason. Because of the use of shared it became difficult to answer the question "is this module being used" with a simple find and replace. Also the question of "where is this module locate" could no longer be answers. The problem is sibling directories or directories down different branches can both refer to 'components/Something' and be referencing different modules and the only way to find out which one was to manually do the hierarchal look up that webpack does. Searching for 'components/Something' may imply the module at hand is being used, but it could be a 'components/Something' else where. You could ofcourse enforce unique module names, but the only reliable/free way to enforce uniqueness is to leverage the file system itself. This circumvents that and for that reason we are leaving this structure.

@loicginoux
Copy link

would you not split your /components folders in 2 differents folders /components and /containers to have a separation more clear between them ?

@felquis
Copy link

felquis commented Aug 16, 2016

@ryanflorence If I have the URL /plans which show all the plans, when I click in one the user goes to /plans/:id

How can I structure it to fit this folder structure?

@kuon
Copy link

kuon commented Aug 22, 2016

I am still struggling a bit with this problem, and I wrote a piece about it: https://the-missing-bit.com/posts/2016/08/21/react-structure/

Copy link

ghost commented Aug 25, 2016

@ryanflorence where do you put not-shared assets, like pictures?

Copy link

ghost commented Aug 28, 2016

babel-plugin-module-resolver will work for tests instead of webpack's modulesDirectories

{
  "plugins": [
    "transform-object-rest-spread",
      ["module-resolver", {
        "root": ["./src/../shared"]
      }]
    ]
}

@emildimitrov
Copy link

Any feedback on using this with react-native project? In my case i have multi-platform codebase like so
src/
common (common for all platforms)
browser-common ( all common things across browsers )
native-common (all common across ios android etc)
browser-mobile ( we have separate website for mobile )
browser-desktop ( separate for desktop )
native-ios ( ios specific )
native-android (android specific)

THanks

@tarun-dugar
Copy link

tarun-dugar commented Dec 17, 2016

@ryanflorence this is awesome! Also, can you tell me how you would you go about integrating redux with this folder structure?

@wuchangming
Copy link

@kuon, Awesome!

@cdtinney
Copy link

cdtinney commented Feb 22, 2017

Wanted to comment to say that I am finding this structure very helpful. I was struggling to find a good way to share components.

For anyone else reading that's using Jest to test, all you need to do is add the shared folder to moduleDirectories in your Jest config in package.json in order for the shared components imports to be found when testing.

e.g.

    "moduleDirectories": [
      "node_modules", "shared"
    ]

@RohovDmytro
Copy link

RohovDmytro commented May 14, 2017

Hey, /everyone :) What do you all think about this structure in 2017? What is your practical experience with it?

Would be very thankful if you share.

@rafael-sa
Copy link

Bump.
I have the same doubt as @rogovdm.

@kentcdodds
Copy link

I love it.

@vladfrontend
Copy link

@iammerrick What did you leave this structure in favor of?

@jessevdp
Copy link

jessevdp commented Apr 3, 2018

We've made it so that shared resolves the same way with webpack modulesDirectories. This way you don't have to require('../../../../../../../../../../shared/Avatar') you can simply do require('components/Avatar') no matter where you are.

How did you get this to work?

@jeffersonRibeiro
Copy link

@rogovdm @rafael-sa I have the same question. Also does someone have an alternative?

@rchamberlin
Copy link

rchamberlin commented May 17, 2018

@jeffersonRibeiro We have a module called bundle.aliases.js

module.exports = {
  '@components': path.resolve('./app', 'client', 'src', 'components'),
  '@constants': path.resolve('./app', 'client', 'src', 'constants'),
  '@screens': path.resolve('./app', 'client', 'src', 'components', 'screens'),
  '@keyboard': path.resolve('./app', 'client', 'src', 'components', 'keyboard'),
  '@node-modules': path.resolve('./app', 'client', 'node_modules'),
  '@containers': path.resolve('./app', 'client', 'src', 'components', 'containers'),
  '@actions': path.resolve('./app', 'client', 'src', '_actions'),
  '@store': path.resolve('./app', 'client', 'src', '_store'),
  '@reducers': path.resolve('./app', 'client', 'src', '_reducers'),
  '@modules': path.resolve('./app', 'client', 'src', 'modules'),
  '@selectors': path.resolve('./app', 'client', 'src', '_selectors')
}

and then in our webpack config we have:

const alias = require(path.resolve(process.cwd(), 'app', 'config', 'bundle.aliases.js'))
resolve: {
  alias
}

@mikebarnhardt
Copy link

I suggest using utils or utilities instead of util to avoid trying to pull from Node's util module when using SSR (such as Razzle).

@JulianSuringa
Copy link

I think the components is isolated to each component name is not the best option for me its look like a drill components 3 or more layer for nesting is not good for readability must better to keep it simple in the directory and I believe this one https://marmelab.com/blog/2015/12/17/react-directory-structure.html.

@tarang9211
Copy link

Hi guys, is there an example for the folder structure? I am a little confused.

@gnapse
Copy link

gnapse commented Jul 26, 2018

This is great, but unfortunately it would break an editor's ability to track what you're importing and from where. vscode for instance is able to give you useful feedback because it "understands" javascript imports, and gives you auto-complete. Even more so if using TypeScript. Is this configuration something that an editor like that would pick up too?

@coderitual
Copy link

@ryanflorence

Hey! Are you still using this approach for current projects? It looks nice but I'm wondering if you have applied some modifications maybe.
Cheers!

@yssong119
Copy link

It's great. I am applying this approach to my project. Thanks.

@malamoney
Copy link

So it seems @ryanflorence abandoned this thread a while back. Several people asking to see an example application using the structure, but I have not seen one provided. Would love to see this in action. Anyone still using this approach?

@seongjin605
Copy link

thank you.
i am using nuxt-js.
great structure :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment