34

Optimal file structure for React applications

 5 years ago
source link: https://www.tuicool.com/articles/hit/qY7NvaY
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
i6vUB3I.png!web2m6reuR.png!web

Dan Abramov famously officialized the file structure for React applications as “ move files around until they feel right .” I do not want to disagree with this point. I agree wholeheartedly. However, despite Dan’s advice, the question of optimal file structure still gains traction frequently. Despite absolute freedom, developers are still uncomfortable with exploring new territories; and I think they have a point. It’s a lot of work to refactor a code base for a file structure change, and it takes a lot of trial and error to find one you like. It would be beneficial to know some ground rules before mapping out your expedition — what have those who came before you discovered?

Over the past two years, I’ve gained extensive experience using React from personal projects to corporate settings; from teams of 2 to teams of 16; from React 0.12 to React 16.8; from Redux to ReactN ; from the browser to React Native. No matter the context of the React application, the structure “feels right” under all of these conditions.

This article is an opinion piece for what file structure has worked best for me and my teams after our own trial and error. You are more than welcome to adjust it for your own use case. I am interested in maintaining this article as a living document of what has worked best for the React community, under what conditions, and why. Please share your file structure discoveries in the comments below. :speech_balloon:

Create React App :construction_worker|type_5:‍♂️

amqQ3iQ.png!webeYFzeaJ.png!web

I am a huge fan of the create-react-app command. It requires hardly any changes out of the box, and its popularity makes for a foundation recognizable to many developers.

This is the file structure created by create-react-app as of 2.1.5 :

my-app
├── build
├── node_modules
├── public
│   ├── favicon.ico
│   ├── index.html
│   └── manifest.json
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   └── serviceWorker.js
├── .gitignore
├── package.json
└── README.md

This is a solid start.

  • build is the location of your final, production-ready build. This directory won’t exist until you run npm build or yarn build . The contents of this folder should be ready-to-ship without any interaction on your part.
  • node_modules is where packages installed by NPM or Yarn will reside.
  • public is where your static files reside. If the file is not imported by your JavaScript application and must maintain its file name, put it here. Files in the public directory will maintain the same file name in production, which typically means that they will be cached by your client and never downloaded again. If your file does not have a filename that matters — such as index.html , manifest.json , or robots.txt — you should put it in src instead.
  • src is where your dynamic files reside. If the file is imported by your JavaScript application or changes contents, put it here. In order to make sure the client downloads the most up-to-date version of your file instead of relying on a cached copy, Webpack will give changed files a unique file name in the production build. This allows you to use simple, intuitive file names during development, such as banner.png instead of banner-2019-03-01-final.png . You never have to worry about your client using the outdated cached copy, because Webpack will automatically rename banner.png to banner.unique-hash.png , where the unique hash changes only when banner.png changes.

From here, create-react-app gives us total freedom. I’ll walk through the file structure that has worked the most cooperatively with the projects on which I’ve worked.

Tweaking Create React App :wrench:

jA7Bnuy.png!webnYjeaen.png!web

One of the most important and agreed upon structures for a React project is to have a components directory for storing your Components. Some developers use two directories — one for stateful Components and one for stateless Components. As an opinionated article, I’ll discuss what I’ve found most intuitive: a single directory for Components. Keep related code as close as possible. That is where we’ll move App.js and its siblings.

src
├── assets
└──images
└── logo.svg
├── components
│ └── app
├── app.css
├── app.js
└── app.test.js
├── index.css
├── index.js
└── service-worker.js

With this new structure, the files directly under src are the entry files. This is where Webpack starts. What is your project? It has a base style sheet, it includes a service worker, and it renders a React application to the DOM.

Your React Components can now be found in the components directory, which we’ll discuss in more depth later.

Your assets, which are dependencies shared by your application — such as SASS mixins, images, etc. — can go in the assets directory. This provides a single location for storing files that could be seen as external to the project itself. If your branding needs to change, the colors can all be adjusted in the centralized SASS directory, your banners can all be adjusted in the centralized images directory. If you need a new build for each localization supported, all the language files can be stored in the centralized localization directory, all the images that contain text can be adjusted in the centralized images directory. It can be a big convenience to group these related, non-JavaScript files together in your otherwise JavaScript project. As a JavaScript developer on a JavaScript project, the management of these non-JavaScript resources are typically handled by others — graphic designers, translators, accessibility consultants, or public relations representatives. Keeping the files close allows them to easily be dropped in and out and ensures that no file is left behind in a process to override a dated dependency, such as the migration from one colored theme to another or the localization from original English to newly-supported Spanish.

The project needs another important addition: a utilities directory. This is a folder full of helper functions that are used globally. Keep your code DRY (Don’t Repeat Yourself) by exporting repeated logic to a singular location and importing it where used. Parts of your application can now share logic without copy-pasting by placing shared logic in this utilities directory. This directory can go by any name, and I’m not picky about it. I don’t think it makes sense to say that one name is better than the others, but it is a directory that needs to exist. src/helpers , src/packages , src/utils . I’ve seen all of the above used in enterprise settings.

Our repository now looks something like this:

my-app
├── build
├── node_modules
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── assets
│ │ └──images
│ │ └── logo.svg
│ ├── components
│ │ └── app
│ │ ├── app.css
│ │ ├── app.js
│ │ └── app.test.js
│ ├── utils
│ ├──
index.css
│ ├── index.js
│ └── service-worker.js
├── .gitignore
├── package.json
└── README.md

The Component Directory :cake:

jiu6Znr.png!web3yANRbn.png!web

The structure of the Component directory is likely the most important one and the reason someone would look up how to structure a React application, an application made of a collection of Components. We’ve established that Components reside in src/components/component-name , but then what?

A sensible decision is to name the Component’s file index.js . This allows you to import it via src/components/component-name , despite that being a directory . When importing a directory, the index.js file is imported. An easily overlooked problem with this approach is that, when editing multiple Components at once in your editor, the tabs are all labeled index.js . This is often useless and impossible to work with.

r2aqI3m.png!web
index.js / index.js / index.js / index.js / index.js / dropdown.js

Visual Studio Code is aware of this problem and will display directory names alongside file names that are the same. Other editors won’t, and you shouldn’t punish other developers on your project for not using the same editor you do. At the same time, the mass repetition of index.js in your tab navigation is wasted space. We can fix this problem and another at the same time. Let’s look closer.

A Component typically involves more than one file. You have the stateless (“dumb”) Component, the stateful container, perhaps a Redux-connecting HOC, SASS files or JSS stylesheets, child Components, or even their own utilities that aren’t shared by other Components. This is precisely why the src/components directory is full of component-name directories and not single component-name.js files.

During development, a Component tends to change hierarchy rapidly. Does it manage its own state, connect to the global store, or does a parent Component pass it a state as props? Does it do just one, any two, or all three of these things? As a separation of concerns and with the readability of file splitting, each of these things should be their own Component and own file. But we only have one component-name/index.js , so which gets to be there?

Logically, the topmost Component in the hierarchy should take that file. It makes sense that the global-connecting Component be component-name/index.js before passing the global state to the container Component at component-name/component-name-container.js before passing the local state to stateless Component at component-name/component-name-view.js .

The problem my teams have run into with this approach has been refactoring. During development, that global state may leave the Component. It may be handled by the parent Component instead and passed as props. It may move to local state. It may not exist at all originally. There is just the view Component as the entry point located at component-name/index.js . Then, once the global state has to be added, it forces us to move the entire file from index.js to component-name-view.js and replace the entry point with the global state connector. It’s so much work to move these files around frequently, and it makes for hideous pull requests and file diffs.

After “moving things around until they felt right,” the solution we discovered is that index.js should do one thing: export { default } from './component-name-topmost.js'; . You put your stateless Component in component-name-view.js , your container Component in component-name-container.js , your global state Component in component-name-redux.js and simply export whichever happens to be the topmost Component at any point in time. You often start with just a stateless Component, so you are exporting view.js . Suddenly, scope demands state, so now you have container.js . You just change view to container in the export statement, and you’re done. Suddenly, scope demands global state, so now you have redux.js . You can just change container to redux in the export statement, and you’re done.

my-app
└──
src
└── components
└── component-name
├── component-name.css
├── component-name.scss
├── component-name-container.js
├── component-name-redux.js
├── component-name-styles.js
├── component-name-view.js
└── index.js

To summarize, we end up with the above structure.

  • component-name.css is a straight-forward CSS file imported by your stateless view Component.
  • component-name.scss is a straight-forward SASS file imported by your stateless view Component.
  • component-name-container.js is your business logic and state management as handled before being sent to the stateless view Component.
  • component-name-redux.js is the mapStateToProps , mapDispatchToProps , and connect functionality of Redux. If you use an alternative global state management tool, give it a similar file name, such as component-name-mobx.js . This allows you to harness multiple global states (if necessary, though not recommended) and allows you to easily swap global state managers in the future.
  • component-name-styles.js is your JSS. I’ve used this file extensively for storing Material UI withStyles HOCs and JSS.
  • component-name-view.js is your stateless view Component. For the majority of cases, this Component should be able to be pure functional Component (no hooks!).
  • index.js is your entry point for importing your Component. It contains nothing but an export statement that points to the topmost Component at any point in time, because the topmost Component changes often during development.

Your component-name directory can have its own utils as well. This allows you to code split. Even if you aren’t using those helper functions or constant definitions in multiple locations, stripping giant chunks of code out of your Components and replacing them with descriptive names is an extremely powerful tool for your development process.

The Subcomponent Directory

If a Component, by definition, is tightly coupled as a child Component of another, I nest it directly in its parent Component’s directory. There is no benefit to cluttering the higher component directory with a Component that is not reusable. It would just be harder to navigate between child and sibling and leave dead code when deprecating a Component.

My portfolio has a section for GitHub repositories. Each item on the list is a Component made up of two Components — the icon and the text. All of these Components are dedicated to being a GitHub repository list for a portfolio. They are not reusable elsewhere on my website, even though they may be made up of reusable Components. My file structure looks like so:

github-repo
├── icon
├── github-repo-icon.scss
├── github-repo-icon-view.js
└── index.js
├── title
├── github-repo-title.scss
├── github-repo-title-view.js
└── index.js
├── github-repo.scss
├── github-repo-view.js

└── index.js

It would be redundantly long to call the icon directory github-repo-icon . I know it’s the GitHub repo icon, because it’s in the github-repo directory. The file names do contain this added text for the aforementioned reason that we do not want a bunch of index.js in the repo. You are likely to have a lot of button.js or icon.js if the file name does not distinguish which it is.

React Router :link:

uQ7VnmJ.png!webnuANJvB.png!web

An additional directory I like to add is src/routes . These are the Components provided directly to react-router 's <Route> s. If I have a page located at charlesstover.com/portfolio , I will have that Component entry point be at src/routes/portfolio/index.js . It may use Components from src/components , but route entry points are unique in that their best name is where they are not what they do . Because of this, they get the special treatment of the routes directory.

Tests :page_facing_up:

Lastly, unit tests. Continuing the principle of keeping related files together, you should store them alongside your tested files. This allows you to easily navigate from your Component to its test file, import the tested files, and keep tests in sync with the files they are testing.

my-app
└──
src
└── components
└── component-name
├── component-name-container.js
├── component-name-container.test.js
├── component-name-redux.js
├── component-name-redux.test.js
├── component-name-view.js
└── component-name-view.test.js

Conclusion :end:

If you liked this article, feel free to give it a clap or two. It’s quick, it’s easy, and it’s free! If you have any questions or great file structure input, please leave them in the comments below.

To read more of my columns, you may follow me on LinkedIn and Twitter , or check out my portfolio on CharlesStover.com .


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK