Want to explore the rules and best practices for structuring React projects and unlock the full potential of your development process? Read on.
In the world of web development, a well-organized project structure is essential for the success and maintainability of any React application. At Infinum, we have developed a comprehensive stack that serves as the foundation for our projects. However, the principles and guidelines we follow can be applied universally to any React project.
Our stack includes powerful tools and libraries:
- Next.js
- TypeScript
- Datx
- JSON:API
- SWR
- Chakra UI
- React Hook Form
- React Testing Library and Jest for unit and integration tests
- Storybook
- I18Next
- Plop Next.js Generators
Leveraging these technologies allows us to build robust and scalable applications.
To showcase the effectiveness of our React project structure, we maintain an example repository on GitHub. This repository serves as a practical reference and demonstrates how our project structure aligns with industry best practices.
In this blog post, we will dive into the key components of our React project structure, explaining how each tool and library contributes to the overall organization, maintainability, and efficiency of our React applications.
Whether you are using our stack or working with different technologies, the insights shared here will help you establish a solid foundation for your React projects.
Organizing components in React project structure
When it comes to organizing components in your React application, a well-defined structure is crucial for maintainability and scalability. By following a consistent and intuitive folder structure, you can easily locate and manage your components, promoting code reuse and modularity. Let’s explore some best practices for organizing your components effectively.
UI Components
When adding UI components, you should be able to group them into three root domains:
1
core
– primitives, low level components
2
shared
– components that are shared across the whole app
3
features
– root folder for components based on a specific feature (could be scoped by page or island)
Folder naming rules:
1
kebab-case
folder name indicates domain name
2
PascalCase
folders and filenames should be used for naming components
src
├── components
│ ├── core
│ │ ├── Section
│ │ │ └── Section.tsx
│ │ └── Card
│ │ └── Card.tsx
│ ├── features
│ │ ├── home
│ │ │ ├── HomeHeaderSection
│ │ │ │ └── HomeHeaderSection.tsx
│ │ │ └── HomeTodoListSection
│ │ │ └── HomeTodoListSection.tsx
│ │ └── todo
│ │ ├── TodoHeaderSection
│ │ │ └── TodoHeaderSection.tsx
│ │ └── TodoCreateFormSection
│ │ └── TodoCreateFormSection.tsx
│ └── shared
│ ├── fields
│ │ └── TextField
│ │ └── TextField.tsx
│ ├── todo
│ │ ├── TodoCard
│ │ │ └── TodoCard.tsx
│ │ ├── TodoList
│ │ │ └── TodoList.tsx
│ │ └── TodoCreateForm
│ │ └── TodoCreateForm.tsx
│ └── utilities
│ ├── BugsnagErrorBoundary
│ │ └── BugsnagErrorBoundary.tsx
│ └── Meta
│ └── Meta.tsx
└── pages
├── index.tsx
└── todo
└── [id]
└── index.tsx
The Core domain
We can refer to these components as atoms, the smallest building blocks. They are highly reusable and composable. If you need some inspiration on how to split components into small segments, you can check the Open UI standard proposal. Components could be designed as Compound Components or “Black-Box” Components with good inversion of control interface, like ReactSelect.
Here are some examples of core components:
Components | Parts | Description |
Card | Card , CardImage , CardImageOverlay , CardTitle , CardDescription , … | From these parts, you’ll be able to compose multiple more specific molecules like ProductCard or UserCard . |
Section | Section , SectionHeader , SectionBody , .. | These might have multiple background schemes like dimmed , inverted , light . |
Search | Search , SearchInput , SearchEmpty , SearchResults , … | Search uses context to provide shared state to other parts. SearchInput renders input and it could be placed anywhere in the DOM structure (for example, in the page Header ). SearchEmpty and SearchResults handle switching between states and showing the result. |
ReactSelect | ReactSelect , ./components/ClearIndicator , ./components/Control , … | The list of custom components can be found here. |
Folder structure:
src
└── components
└── core
├── Card
│ └── Card.tsx
├── Section
│ └── Section.tsx
├── Search
│ └── Search.tsx
└── ReactSelect
└── ReactSelect.tsx
The Shared domain
These components can be considered molecules. They are more specific, and built out of atoms, i.e. core components. They can be shared between feature components, and encapsulate some specific logic of a feature.
We can split them into three domains:
1
UI
– higher order user interface components
2
Entity
– UI representation of data models
3
Utility
– headless utility components
Shared UI domain
Component name is always composed out of two parts, Context
+ Domain
. For example, InputField
where Input
is context and Field
is domain.
Domains | Components | Description |
fields | Card , CardImage , CardImageOverlay , CardTitle , CardDescription , … | Specific form fields prepared to be used with the React Hook Form library. Built out of multiple parts, for example InputGroup , InputLeftElement , Input from Chakra UI. |
overlays | UnsupportedBrowserOverlay , BugsnagErrorOverlay | Components that cover the whole page and prevent users from interacting with the page to some degree. |
layouts | MainLayout , AdminLayout | Components that are shared across pages and render the application shell (navigation and footer). |
messages | NoResultsMessage , EmptyListMessage , LoadingMessage , ErrorMessage | Reusable message components that could be shared across pages for handling empty list results, loading states or ErrorBoundaries fallback. |
navigations | MainNavigation , AdminNavigation | Different navigations used in layouts to support different app shell styles. They can handle user logged-in/logged-out states and mobile/desktop layouts. |
footers | MainFooter , AdminFooter | Different footers used in layouts to support different app shell styles. Serves the same purpose as navigations . |
panels | ArticlesPanel , EventPanel , EventSidebarPanel , GroupPanel | Specific panels that hold filtering dropdowns for narrowing down list results. Usually consists of a core Panel compound component for sharing styles and sorting dropdowns. |
markdowns | ArticleMarkdown , AnnouncementMarkdown | Components that handle parsing of the markdown and styling of the generated HTML. |
icons | PlusIcon , TrashIcon | SVG icons used throughout the application. The icons should be named by what they are, not where they are used, e.g. TrashIcon instead of DeleteIcon or ExclamationCircleIcon instead of ErrorIcon . |
Folder structure:
src
└── components
└── shared
└── fields
└── TextField
└── TextField.tsx
Shared Entity domain
We can also refer to these components as molecules, but they are tied to a certain entity, for example an API data model, Algolia resource, or Google Map entity.
Component names are always composed out of two parts, Entity
+ Context
. For example, TodoList
where Todo
is an entity and List
is the context.
Domains | Components | Description |
todo | TodoList , TodoCreateForm , TodoCard , … | |
user | UserList , UserCreateForm , UserCard , … | Primarily, they should accept an entity prop like this <UserCard user={user} /> where user is a resource from the API, or on rare occasion they could accept primitive props like resourceId and perform resource fetching via SWR . |
ticket | TicketList , TicketCreateForm , TicketCard , … |
Folder structure:
src
└── components
└── shared
└── todo
└── TodoCard
└── TodoCard.tsx
Shared Utility domain
Utility components are usually headless components, which means that they don’t have any impact on the UI itself, but are still reusable declarative React components.
Domains | Components | Description |
utilities | Meta , BugsnagErrorBoundary | Meta inserts meta tags into the document head . BugsnagErrorBoundary catches the error, triggers the Bugsnag report and renders fallback components. |
For example, let’s see how the Meta
component is used for injecting meta tags inside the document <head>
.
import React, { FC } from 'react';
import Head from 'next/head';
export const Meta: FC = () => {
return (
<Head>
<link rel='icon' type='image/png' sizes='32x32' href='/favicon-32x32.png' />
<link rel='icon' type='image/png' sizes='16x16' href='/favicon-16x16.png' />
<link rel='shortcut icon' href='/favicon.ico' />
<title>Infinum</title>
</Head>
);
};
Folder structure:
src
└── components
├── shared
│ ├── utilities
│ │ └── Meta
│ │ └── Meta.tsx
…
The components folder
When adding a components
folder, you are basically extracting smaller chunks of your main React component that are only going to be used in that component and nowhere else.
Note: There should only be one level of component nesting inside the components
folder. In fact, we are considering renaming this folder from components
to elements
to avoid confusion with the project root components
folder, but this is still to be defined.
In this example, the MainTable
component has a unique header component that should be placed inside the /components
folder because it contains some styles, translations and an icon.
export const TableHeader: FC<FlexProps> = (props) => {
const { t } = useTranslation();
return (
<Flex align='center' p={20} {...props}>
<Heading size='md' colorScheme='secondary' as='h3'>
{t('table.title')}
</Heading>
<Button leftIcon={<ArrowForwardIcon />} colorScheme='teal' variant='solid'>
{t('table.viewAll')}
</Button>
</Flex>
);
};
The folder structure should look something like this:
src
└── components
└── features
└── some-feature
└── MainTable
├── components
│ └── TableHeader.tsx
└── MainTable.tsx
Elements
If you have a lot of style declarations inside your component file, enough to make the file difficult to read, you should create a separate file ComponentName.elements.ts
and store your custom components there.
Example:
.
└── WelcomeCard
├── WelcomeCard.tsx
└── WelcomeCard.elements.ts
In the following example, WelcomeCardLayoutOverlay
is a Pure Functional Component because the Chakra UI factory doesn’t allow the custom isOpen
prop.
import { chakra, HTMLChakraProps, ThemingProps, useStyleConfig } from '@chakra-ui/react';
export const WelcomeCardLayout = chakra(Grid, {
baseStyle: {
gridTemplateRows: '1fr min-content min-content',
gridTemplateColumns: '1fr min-content',
rowGap: '16px',
},
});
export const WelcomeCardLayoutContent = chakra(GridItem, {
baseStyle: {
position: 'relative',
gridRowStart: '1',
gridRowEnd: 'auto',
gridColumnStart: '1',
gridColumnEnd: 'auto',
},
});
export interface WelcomeCardLayoutOverlayProps extends TMLChakraProps<'div'> {
isOpen?: boolean;
}
export const WelcomeCardLayoutOverlay = forwardRef<WelcomeCardOverlayProps, 'div'>(
({ isOpen, ...rest }, ref) => {
const height = isOpen ? { base: '300px', md: '500px' } : null;
return (
<GridItem ref={ref} h={height} position='relative' column='1 / 3' row='1 / 2' {...rest} />
);
}
);
Moving component parts to .elements.tsx
should be the last step in the development process and it should only be used for organizational purposes, i.e. when the main component becomes cluttered and unreadable.
Here are some rules that you should follow when creating elements:
.elements.tsx
should only be used for organizational purposes- Custom components are tightly coupled with the root component and they should not be used in the outside scope
- Mixture of
chakra factory
andFunction Components
is allowed - Using hooks inside elements is not recommended
- Wrapping in forwardRef is recommended but not mandatory (you need this in case you want to access real DOM element in the root component)
Note: For more information check the Chakra UI – Style Props section in our Handbook.
Different component layouts for different screen sizes
In most cases we’re striving to create responsive components that consist of one DOM structure that is going to be rendered on all screen sizes by utilizing CSS-only solutions. Work closely with your designer to achieve this.
However, there are cases when we need to create different DOM structures for different screen sizes.
Collocating different layouts in one component
With the help of Chakra UI Display helper props:
export const UserCard = (props) => {
return (
<Box {...props}>
<Box hideFrom='md'>
// Mobile layout
</Box>
<Box hideBelow='md'>
// Desktop layout
</Box>
</Box>
)
}
Different component layouts for different screen sizes in separate layout
components
In this case, inside a specific component, we could add a subfolder layouts
(not to be confused with the actual layout described below) to define how our component would look like on a specific media query.
We advise using this approach only for those layouts that would add too much complexity when using Chakra UI Display helper props. We realize that this approach usually results in a lot of code duplication, so it’s best to use it only when necessary.
.
└── admin
└── UserCard
├── layouts
│ ├── UserCard.mobile.tsx
│ └── UserCard.desktop.tsx
└── UserCard.tsx
For this case, inside UserCard.tsx
we would have something like this:
With the help of Chakra UI Display helper props:
export const UserCardMobile = () => (
<Box hideFrom='md'>
// Mobile layout
</Box>
)
export const UserCardDesktop = () => (
<Box hideBelow='md'>
// Desktop layout
</Box>
)
export const UserCard = () => {
return (
<Fragment>
<UserCardMobile />
<UserCardDesktop />
</Fragment>
)
}
With help of Chakra UI Show/Hide:
import { Show, Hide } from '@chakra-ui/react';
export const UserCard = () => {
return (
<Fragment>
<Hide above="md">
<UserCardMobile />
</Hide>
<Show above="md">
<UserCardDesktop />
</Show>
</Fragment>
)
}
Extracting utility functions and hooks
Sometimes, you will have complex functions or effects inside your React component that will affect the readability of your component. In that case, you should extract them into separate files: *.utils.ts
and *.hooks.ts
.
The main goal of these files is to store functions and hooks that are specific for that component, so we could keep our root hooks
and utils
folders clean and for global usage purposes only. If you would like to know more, we’ve written about React hooks in class components before.
Folder structure example:
.
├── …
└── AlbumsCarousel
├── AlbumsCarousel.tsx
└── AlbumsCarousel.utils.ts
└── AlbumsCarousel.hooks.ts
Cluttered component example:
export const AlbumsCarousel = (props) => {
const [isPlaying, setIsPlaying] = useState(true);
const [highlighted, setHighlighted] = useState(null);
const [zoom, setZoom] = useState(1);
const modals = useModals();
const carouselRef = useRef();
const showLoginModal = () => {
modals.open(Modals.Login);
};
const highlightAlbum = (id: number) => {
setIsPlaying(false);
setHighlighted(id);
carouselRef.current.collapseItems();
};
const formatReleaseDate = (startDate: string, endDate: string) => {
const start = new Date(startDate);
const end = new Date(endDate);
const timezonedStart = new Date(start.valueOf() + start.getTimezoneOffset() * 60 * 1000);
const timezonedEnd = new Date(end.valueOf() + end.getTimezoneOffset() * 60 * 1000);
if (isSameDay(timezonedStart, timezonedEnd)) {
return `${format(timezonedStart, 'd MMMM yyyy')}`;
}
if (!isSameYear(timezonedStart, timezonedEnd)) {
return `${format(timezonedStart, 'd MMMM yyyy')} ${String.fromCharCode(8212)} ${format(
timezonedEnd,
'd MMMM yyyy',
)}`;
}
if (!isSameMonth(timezonedStart, timezonedEnd)) {
return `${format(timezonedStart, 'd MMMM')} ${String.fromCharCode(8212)} ${format(
timezonedEnd,
'd MMMM yyyy',
)}`;
}
return `${format(timezonedStart, 'd')} ${String.fromCharCode(8212)} ${format(
timezonedEnd,
'd MMMM yyyy',
)}`;
}
return (
//...
<div>{formatReleaseDate(props.startDate, props.endDate)</div>
//...
)
}
Clean component:
import { formatReleaseDate } from './AlbumsCarousel.utils.ts';
export const AlbumsCarousel = (props) => {
const [isPlaying, setIsPlaying] = useState(true);
const [highlighted, setHighlighted] = useState(null);
const [zoom, setZoom] = useState(1);
const modals = useModals();
const carouselRef = useRef();
const showLoginModal = () => {
modals.open(Modals.Login);
};
const highlightAlbum = (id: number) => {
setIsPlaying(false);
setHighlighted(id);
carouselRef.current.collapseItems();
};
return (
//...
<div>{formatReleaseDate(props.startDate, props.endDate)</div>
//...
)
}
Note: You can find out more about hook encapsulation in our handbook.
Layouts
With Layouts, we generally define the overall page structure that will be presented to the user based on a specific auth state or a specific page.
For example, your app can have an Admin dashboard on the /admin
route which has a fixed header and sidebars on both right and left, and scrollable content in the middle (you’re most likely familiar with this kind of layout).
You define your different layouts inside the layouts
folder:
.
└── components
└── layouts
├── AdminLayout
│ └── AdminLayout.tsx
├── MainLayout
│ └── MainLayout.tsx
└── BlogLayout
└── BlogLayout.tsx
Then, when creating your routes (pages), you wrap your page in the layout that represents the current page:
// pages/index.tsx
export default function Home() {
return <MainLayout>...</MainLayout>;
}
// pages/admin.tsx
export default function Admin() {
return <AdminLayout>...</AdminLayout>;
}
Note: This section is preparation for the new Next.js App folder structure.
Setting up the global store
At Infinum we use our own solution for global data management and remote data fetching, called Datx. You can read more about working with JSON API, hassle-free in one of our previous blog posts.
Our Datx store and models will be placed in the root of the src
folder as follows:
src
├── models
│ ├── User.ts
│ └── Session.ts
└── datx
└── create-client.ts
Setting up theming and styles
When creating styles for your core components, you will create the components
folder inside the styles folder. The styles folder will contain all core stylings and the theme setup.
src
└── styles
└── theme
├── index.ts # main theme endpoint
├── styles.ts # global styles
├── foundations # colors, typography, sizes...
│ ├── font-sizes.ts
│ └── colors.ts
└── components # components styles
└── button.ts
Tests
Here are a couple of quick rules to follow when organizing test files:
- Components, utils, fetchers, etc. should have a test file in the same folder where they are placed
- When testing pages, create the
__tests__/pages
folder, because of how Next.js treats the pages folder - All mocks should be placed in the
__mocks__
folder
For other in-depth guides for testing, take a look at the testing guide.
Your folder structure should look something like this:
src
.
├── __mocks__
│ └── react-i18next.tsx
├── __tests__
│ ├── pages
│ │ └── user.test.ts
│ └── test-utils.tsx
├── pages
│ └── user.ts
└── components
└── shared
└── core
└── Button
├── Button.test.tsx
└── Button.tsx
A well-defined React project structure goes a long way
Organizing your React project folder structure is crucial for maintainability and scalability. At Infinum, we have established a well-defined structure that categorizes components into different domains: core, shared, and features.
By embracing Chakra UI and leveraging concepts such as compound components and the inversion of control interfaces, we foster the creation of highly composable and customizable core components.
Adopting a consistent and well-defined React native project structure for components enables development teams to easily locate and understand the purpose of each component. This approach promotes code reuse, modularity, and improves collaboration among team members. Ultimately, an organized folder structure paves the way for a more maintainable and scalable codebase.
The complete folder structure presented in this article can be found in our handbook.