Thibaud’s blog

Notes, thoughts, and open-source software

Edit

Conventions to organize React projects

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
  1. polls is a functional area
  2. Within this, urls.py does all routing-level logic for polls
  3. 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, and Button.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 in Button.js.
  • 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. Do polls.models.js. Don’t urls.js. Do polls.urls.js.
    • I always use text search and filters to navigate through code, this makes it much easier.

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 the components 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

References