I’ve spent a lot of time thinking of how best to structure React projects over the last few years. This is especially important when React is a central part of the project’s front-end tech stack, as React in itself has very few opinions about how to organize code (it’s a library, not a framework). Here are my notes.
Inspiration
My favourite file structure is that of Django. In particular,
- At the top level, dividing code by functional area
- Within each functional area, the files’ names follow an established convention based on non-functional considerations.
- If needed, code can then be broken up by functional area for each type of file
What I really like about those conventions is that they give the code as much room to grow as possible – add more apps as the project grows, remove whole apps if they’re no longer needed, and sub-divide individual apps if they start getting too big.
Here is what this looks like in practice:
demo
├── feedback
│ ├── templates
│ ├── urls.py
│ └── views.py
├── polls
│ ├── admin.py
│ ├── migrations
│ ├── models
│ │ ├── questions.py
│ │ └── choices.py
│ ├── templates
│ │ └── polls
│ │ ├── question_list.html
│ │ └── results.html
│ ├── urls.py
│ └── views.py
├── settings.py
└── urls.py
polls
is a functional area- Within this,
urls.py
does all routing-level logic forpolls
- If needed, that non-functional can be broken up into further features-oriented files like with
models
, which has separate files.
The overarching rules
From that Django baseline, here are rules I like to follow when adapting this to arbitrary technologies.
Folder structure
- Co-locate code by feature rather than file type as much as possible.
Button.js
,Button.css
,Button.test.js
, andButton.md
should all live side-by-side.- User profile logic, avatar & bio UI components, profile details update forms – should all be close one-another if possible.
- Avoid files that are just importing and re-exporting code.
- These are useful for libraries that need a clean API, and an overhead for our first-party project code.
- Modern IDEs support automatically updating file paths as files move, and auto-completing imports.
- Extract code that is not feature-specific into a reusable “shared” folder.
- Consider having another “modules” folder for code that is so un-specific to your project that it could just as well come from a dependency.
Naming conventions
- Files should be named after their default export as much as possible.
- The
Button
component lives inButton.js
.
- The
- For ancilliary files, take the name of the main file and suffix it with the “type”.
Button.test.js
,Button.stories.js
,Button.scss
- Files that represent non-functional considerations can be named after the “app” they are in, for example
polls.models.js
- Each file name must be unique for the whole project, and convey what the file contains without context.
- Don’t
models.js
. Dopolls.models.js
. Don’turls.js
. Dopolls.urls.js
. - I always use text search and filters to navigate through code, this makes it much easier.
- Don’t
Import
- Always import by relative file path.
- Those imports are compatible with the most tools – IDEs, linters, SCMs
- They can be automatically generated by IDEs.
- They are very short when code is co-located with its immediate dependencies, which is the most common type of import.
React specificites
- Components should be in their
components
folder for each app - If your tools allow it, always start by co-locating the component’s code, documentation, styles, data dependencies, all in the same file.
- Break it down into separate co-located files if it gets too big.
- Components should have their own folder if the component is made up of more than one file (
Button.js
,Button.test.js
) - If React is your main UI building block, consider having a separate
pages
folder for page-level components, if thecomponents
folder gets too big.
Redux specificities
- Follow the Ducks methodology, ideally using Redux Toolkit.
- Co-locate
connect
code in the component’s source. No one wants to jump back and forth between “container” and “dumb” components.
In practice
Here is a bigger React project structured with this methodology:
.
├── admin.entry.js
├── client.entry.js
├── counsellor.entry.js
├── booking
│ ├── booking.actions.js
│ ├── booking.constants.js
│ ├── booking.models.js
│ ├── booking.models.test.js
│ ├── booking.reducer.js
│ ├── booking.routes.js
│ ├── booking.types.js
│ └── components
│ ├── AvailabilityPicker
│ │ ├── AvailabilityPicker.js
│ │ ├── AvailabilityPicker.scss
│ │ ├── AvailabilityPicker.stories.js
│ │ ├── CalendarButton
│ │ │ ├── CalendarButton.js
│ │ │ └── CalendarButton.scss
│ │ ├── CounsellorSlots.js
│ │ ├── LoadingOverlay.js
│ │ ├── LoadingOverlay.scss
│ │ ├── SlotOverlay.js
│ │ ├── SlotOverlay.scss
│ │ ├── SlotPicker.js
│ │ └── SlotPicker.scss
│ ├── BookingContent
│ │ ├── BookingContent.js
│ │ └── BookingContent.test.js
│ ├── BookingFlow.js
│ ├── ChooseAvailability.js
│ ├── ConfirmBooking
│ │ ├── ConfirmBooking.js
│ │ └── ConfirmBooking.scss
│ ├── CounsellorBioModal
│ │ ├── CounsellorBioModal.js
│ │ ├── CounsellorBioModal.scss
│ │ └── CounsellorBioModal.stories.js
│ ├── ProgressBar
│ │ ├── ProgressBar.js
│ │ └── ProgressBar.scss
│ └── Register
│ ├── ProvisionalBookingCard.js
│ ├── Register.js
│ ├── Register.scss
│ ├── RegisterPersonal.js
│ ├── RegisterPersonal.test.js
│ └── RegisterSituation.js
├── counsellors
│ ├── components
│ │ ├── ClientDetail.js
│ │ ├── ClientHistory
│ │ │ ├── ClientHistory.js
│ │ │ ├── ClientHistory.scss
│ │ │ ├── ClientHistory.stories.js
│ │ │ └── ClientHistoryCard
│ │ │ ├── ClientHistoryCard.js
│ │ │ └── ClientHistoryCard.scss
│ │ ├── CounsellorDashboard.js
│ │ ├── Timeline
│ │ │ ├── Timeline.js
│ │ │ ├── Timeline.scss
│ │ │ ├── Timeline.stories.js
│ │ │ └── Timeline.test.js
│ │ └── TimelineCard
│ │ ├── TimelineCard.js
│ │ ├── TimelineCard.scss
│ │ ├── TimelineCard.stories.js
│ │ ├── TimelineCard.test.js
│ │ └── **snapshots**
│ │ └── TimelineCard.test.js.snap
│ ├── counsellors.actions.js
│ ├── counsellors.api.js
│ ├── counsellors.reducer.js
│ ├── counsellors.routes.js
│ └── counsellors.types.js
├── dashboard
│ ├── components
│ │ ├── BookingLanding
│ │ │ ├── BookingLanding.js
│ │ │ └── BookingLanding.scss
│ │ ├── CancelBooking
│ │ │ ├── CancelBooking.js
│ │ │ ├── CancelBooking.scss
│ │ │ ├── CancelBookingSuccess.js
│ │ │ └── CancelForm.js
│ │ ├── ChangeBooking
│ │ │ ├── ChangeBooking.js
│ │ │ ├── ChangeBooking.scss
│ │ │ └── ChangeBookingConfirm.js
│ │ ├── Dashboard.js
│ │ ├── DonationRequest
│ │ │ ├── DonationRequest.js
│ │ │ └── DonationRequest.scss
│ │ ├── EndOfSessions
│ │ │ ├── EndOfSessions.js
│ │ │ └── EndOfSessions.scss
│ │ ├── NonVerifiedUserMessage
│ │ │ ├── NonVerifiedUserMessage.js
│ │ │ └── NonVerifiedUserMessage.scss
│ │ └── ViewBookings
│ │ ├── ViewBookings.js
│ │ ├── ViewBookings.scss
│ │ └── ViewBookings.test.js
│ ├── dashboard.actions.js
│ ├── dashboard.api.js
│ ├── dashboard.constants.js
│ ├── dashboard.reducer.js
│ └── dashboard.routes.js
├── main.scss
├── shared
│ ├── api
│ │ ├── timekit.js
│ │ └── video.js
│ ├── components
│ │ ├── AppError
│ │ │ └── AppError.js
│ │ ├── BookingCard
│ │ │ ├── BookingCard.js
│ │ │ ├── BookingCard.mixins.scss
│ │ │ ├── BookingCard.scss
│ │ │ ├── BookingCard.stories.js
│ │ │ └── DNACard
│ │ │ ├── DNACard.js
│ │ │ └── DNACard.scss
│ │ ├── Button
│ │ │ ├── Button.js
│ │ │ └── Button.stories.js
│ │ ├── ClientApp.js
│ │ ├── ContentCard
│ │ │ ├── ContentCard.js
│ │ │ ├── ContentCard.scss
│ │ │ └── ContentCard.stories.js
│ │ ├── CounsellorApp.js
│ │ ├── DashboardWrapper.js
│ │ ├── DatePicker
│ │ │ ├── DatePicker.js
│ │ │ ├── DatePicker.scss
│ │ │ └── DatePicker.stories.js
│ │ ├── ErrorModal
│ │ │ ├── ErrorModal.js
│ │ │ ├── ErrorModal.scss
│ │ │ └── ErrorModal.stories.js
│ │ ├── FormField
│ │ │ ├── FormField.js
│ │ │ ├── FormField.stories.js
│ │ │ ├── RadioGroup.js
│ │ │ └── StyledErrorMessage.js
│ │ ├── Icon
│ │ │ ├── Icon.js
│ │ │ ├── Icon.stories.js
│ │ │ └── Icon.test.js
│ │ ├── InfoBox
│ │ │ ├── InfoBox.js
│ │ │ ├── InfoBox.scss
│ │ │ └── InfoBox.stories.js
│ │ ├── LoadingIndicator
│ │ │ ├── LoadingIndicator.js
│ │ │ ├── LoadingIndicator.scss
│ │ │ └── LoadingIndicator.stories.js
│ │ ├── MainWrapper.js
│ │ ├── Modal
│ │ │ ├── Modal.js
│ │ │ ├── Modal.scss
│ │ │ └── Modal.stories.js
│ │ ├── NoBookings
│ │ │ ├── NoBookings.js
│ │ │ ├── NoBookings.scss
│ │ │ └── NoBookings.stories.js
│ │ ├── NotFound.js
│ │ ├── ScrollToTop.js
│ │ ├── SentryBoundary
│ │ ├── SentryBoundary.js
│ │ ├── ShowBetween
│ │ │ ├── ShowBetween.js
│ │ │ └── ShowBetween.test.js
│ │ ├── mobile-menu.js
│ │ ├── mobile-sub-menu.js
│ │ └── read-more.js
│ ├── fonts
│ │ ├── IF_Std_Bold.ttf
│ │ ├── IF_Std_Bold.woff2
│ │ ├── IF_Std_Light.ttf
│ │ ├── IF_Std_Light.woff2
│ │ ├── IF_Std_Regular.ttf
│ │ └── IF_Std_Regular.woff2
│ ├── images
│ │ ├── backdrop-bluecard.svg
│ │ ├── backdrop-shape.svg
│ │ ├── bottom-right-white-brand-blob.svg
│ │ ├── cancellation.png
│ │ ├── change.png
│ │ ├── checkmark.svg
│ │ ├── chevron-right.svg
│ │ ├── confirmation.png
│ │ ├── email-logo.png
│ │ ├── fr-logo.png
│ │ ├── reminder.png
│ │ └── verification.png
│ ├── sass
│ │ ├── abstracts
│ │ │ ├── _functions.scss
│ │ │ ├── _mixins.scss
│ │ │ └── _variables.scss
│ │ ├── base
│ │ │ ├── _base.scss
│ │ │ ├── _fonts.scss
│ │ │ └── _typography.scss
│ │ ├── components
│ │ │ ├── _blockquote.scss
│ │ │ ├── _call-to-action.scss
│ │ │ ├── _card.scss
│ │ │ ├── _client-detail.scss
│ │ │ ├── _cookie-message.scss
│ │ │ ├── _grid.scss
│ │ │ ├── _icon.scss
│ │ │ ├── _menu-dropdown.scss
│ │ │ ├── _nav.scss
│ │ │ ├── _read-more.scss
│ │ │ ├── _responsive-object.scss
│ │ │ ├── _rich-text.scss
│ │ │ ├── _section.scss
│ │ │ ├── _select-menu.scss
│ │ │ ├── _user-modal.scss
│ │ │ ├── _wrapper.scss
│ │ │ ├── button
│ │ │ │ ├── _button-action.scss
│ │ │ │ ├── _button-rounded.scss
│ │ │ │ ├── _button-slot.scss
│ │ │ │ └── _button.scss
│ │ │ ├── case-load
│ │ │ │ ├── _case-load-header.scss
│ │ │ │ ├── _case-load-table.scss
│ │ │ │ └── _case-load.scss
│ │ │ └── form
│ │ │ ├── _form-checkbox.scss
│ │ │ ├── _form-item.scss
│ │ │ ├── _form-radio.scss
│ │ │ └── _form.scss
│ │ ├── layout
│ │ │ ├── _footer.scss
│ │ │ ├── _header.scss
│ │ │ └── _sidebar.scss
│ │ ├── utilities
│ │ │ └── _utilities.scss
│ │ └── vendor
│ │ └── _normalize.scss
│ └── utils
│ ├── actions.js
│ ├── dates.js
│ ├── dates.test.js
│ ├── delay.js
│ ├── delay.test.js
│ ├── errors.js
│ ├── hooks.js
│ ├── notifications.js
│ ├── phonenumbers.js
│ ├── phonenumbers.test.js
│ ├── storage.js
│ └── storage.test.js
├── store.js
├── stories
│ ├── TemplatePattern.js
│ ├── addons.js
│ ├── config.js
│ ├── middleware.js
│ ├── mocks
│ │ ├── bookings.mock.js
│ │ ├── counsellor-sample-image.jpeg
│ │ ├── counsellors.mock.js
│ │ ├── sample-video.mp4
│ │ └── users.mock.js
│ ├── storyshots.test.js
│ └── webpack.config.js
├── tests
│ ├── assetMock.js
│ ├── environment.js
│ └── setupTests.js
├── user
│ ├── components
│ │ └── UserNav.js
│ ├── user.actions.js
│ ├── user.api.js
│ ├── user.constants.js
│ ├── user.reducer.js
│ └── user.types.js
└── video
├── components
│ ├── AppointmentDetails
│ │ ├── AppointmentDetails.js
│ │ ├── AppointmentDetails.scss
│ │ └── AppointmentDetails.stories.js
│ ├── PreviewPlayer
│ │ ├── PreviewFeed.js
│ │ ├── PreviewPlayer.js
│ │ ├── PreviewPlayer.scss
│ │ └── PreviewPlayer.stories.js
│ ├── SessionStatusForm
│ │ ├── SessionStatusForm.js
│ │ ├── SessionStatusForm.scss
│ │ └── SessionStatusForm.stories.js
│ ├── Timer
│ │ ├── Timer.js
│ │ ├── Timer.scss
│ │ ├── Timer.stories.js
│ │ └── Timer.test.js
│ ├── TwilioVideoPlayer.js
│ ├── Video.js
│ ├── VideoOverlay
│ │ ├── VideoOverlay.js
│ │ └── VideoOverlay.scss
│ ├── VideoPlayer
│ │ ├── VideoPlayer.js
│ │ ├── VideoPlayer.scss
│ │ └── VideoPlayer.stories.js
│ └── VideoRoom
│ ├── ClientVideoRoom.js
│ ├── ClientVideoRoom.stories.js
│ ├── ConfirmEndCall.js
│ ├── ConfirmEndCall.scss
│ ├── CounsellorVideoRoom.js
│ ├── CounsellorVideoRoom.stories.js
│ ├── VideoRoom.js
│ └── VideoRoom.scss
├── video.actions.js
├── video.constants.js
├── video.reducer.js
└── video.routes.js
70 directories, 258 files
Smaller Create React App + Redux Toolkit project:
src/
├── main.entry.tsx
├── main.entry.scss
├── nav
│ ├── components
│ │ ├── NavBar
│ │ │ ├── Logo.svg
│ │ │ ├── NavBar.stories.tsx
│ │ │ └── NavBar.tsx
│ │ └── PrimaryNav
│ │ ├── PrimaryCategory.tsx
│ │ ├── PrimaryNav.stories.tsx
│ │ ├── PrimaryNav.test.tsx
│ │ └── PrimaryNav.tsx
│ ├── nav.data.ts
│ ├── nav.slice.ts
│ └── nav.types.ts
├── shared
│ ├── components
│ │ ├── App
│ │ │ ├── App.test.tsx
│ │ │ ├── App.tsx
│ │ │ └── Dashboard.tsx
│ │ ├── Button
│ │ │ ├── Button.stories.tsx
│ │ │ └── Button.tsx
│ │ └── Release
│ │ ├── Release.module.scss
│ │ ├── Release.test.tsx
│ │ └── Release.tsx
│ ├── shared.constants.ts
│ ├── store.ts
│ └── styles
│ └── theme.ts
└── user
├── components
│ └── AccountInfo
│ ├── AccountInfo.stories.tsx
│ ├── AccountInfo.test.tsx
│ └── AccountInfo.tsx
├── user.data.ts
├── user.slice.ts
└── user.types.ts