Get Your Components in Order with React Project Structure

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:

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:

ComponentsPartsDescription
CardCard, CardImage, CardImageOverlay, CardTitle, CardDescription, …From these parts, you’ll be able to compose multiple more specific molecules like ProductCard or UserCard.
SectionSection, SectionHeader, SectionBody, ..These might have multiple background schemes like dimmed, inverted, light.
SearchSearch, 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.
ReactSelectReactSelect, ./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.

DomainsComponentsDescription
fieldsCard, 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.
overlaysUnsupportedBrowserOverlay, BugsnagErrorOverlayComponents that cover the whole page and prevent users from interacting with the page to some degree.
layoutsMainLayout, AdminLayoutComponents that are shared across pages and render the application shell (navigation and footer).
messagesNoResultsMessage, EmptyListMessage, LoadingMessage, ErrorMessageReusable message components that could be shared across pages for handling empty list results, loading states or ErrorBoundaries fallback.
navigationsMainNavigation, AdminNavigationDifferent navigations used in layouts to support different app shell styles. They can handle user logged-in/logged-out states and mobile/desktop layouts.
footersMainFooter, AdminFooterDifferent footers used in layouts to support different app shell styles. Serves the same purpose as navigations.
panelsArticlesPanel, EventPanel, EventSidebarPanel, GroupPanelSpecific panels that hold filtering dropdowns for narrowing down list results. Usually consists of a core Panel compound component for sharing styles and sorting dropdowns.
markdownsArticleMarkdown, AnnouncementMarkdownComponents that handle parsing of the markdown and styling of the generated HTML.
iconsPlusIcon, TrashIconSVG 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.

DomainsComponentsDescription
todoTodoList, TodoCreateForm, TodoCard, …
userUserList, 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.
ticketTicketList, 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.

DomainsComponentsDescription
utilitiesMeta, BugsnagErrorBoundaryMeta 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 and Function 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>
  )
}

Use of Show/Hide helpers is advisable to be used only for client side rendering. For server side rendering use Display helper props.

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

Note: we have recently released the @datx/swr (doc) package which is a wrapper around SWR library. This is recommended for fetching data from the API using the JSON:API specification.

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.