Project structure
Last modified on Fri 29 Jul 2022

Organizing components

UI Components

When adding UI components, you should be able to group them in two root domains:

  1. core - primitives, low level components
  2. shared - components that are shared all across the 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 components naming
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

Core domain

We can refer to them as atoms, smallest building blocks, highly reusable and composable. You can check the Open UI standard proposal for inspiration how to split components into small segments. 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, .. This 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 handles switching between states and showing the result.
ReactSelect ReactSelect,
./components/ClearIndicator,
./components/Control, ...
The list of custom components can be find here

Shared domain

We can refer to them as molecules. They are more specific components built out of atoms (core components). They could be shared between feature components and encapsulates some specific logic of an feature.

We can split them into three domains: 1. UI - higher order user interface components 2. Entity - UI representation of a 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.

Here are some examples of feature domain names:

Domains Components Description
fields InputField, TextareaField Specific form fields prepared to be used with React Hook Form library. Built out of multiple parts, for example InputGroup, InputLeftElement, Input form Chakra UI
overlays UnsupportedBrowserOverlay, BugsnagErrorOverlay Components that covers the whole page and prevents user to interact with the page in some degree.
layouts MainLayout, AdminLayout Components that are shared across the pages and renders the application shell (navigation and footer)
messages NoResultsMessage, EmptyListMessage, LoadingMessage, ErrorMessage Reusable messages components that could be shared across the 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 could 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 holds filtering dropdowns for narrowing down the list results. Usually consists of core Panel compound component for sharing the styles and sorting dropdowns.
markdowns ArticleMarkdown, AnnouncementMarkdown Components that handles 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 instad of DeleteIcon or ExclamationCircleIcon instead of ErrorIcon

Shared Entity domain

We can refer to them as molecules also, but they are tied to some entity, for example Datx model, algolia resource, google map entity.

Component name is always composed out of two parts Entity + Context, for example TodoList where Todo is entity and List is context.

Domains Components Description
todo TodoList, TodoCreateForm, TodoCard, ... They should accept primitive props like resourceId and do the resource fetching via SWR.
user UserList, UserCreateForm, UserCard, ...
ticket TicketList, TicketCreateForm, TicketCard, ...

Shared utility domain

Utility components usually does not have any visual representation on the screen, but they are still reusable declarative components.

Domains Components Description
utilities Meta, BugsnagErrorBoundary Meta inserts meta tags into document head. BugsnagErrorBoundary catches the error, triggers the Bugsnag report and render fallback component

components folder

When adding components folder, you basically extracting smaller chunks of your main component that are not going to be used anywhere else, only in that component.

Note: There should be only one level of components folder inside the component folder.

Example:

In this example we have the MainTable component that has a unique header component that should be placed inside the /components folder because it has 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 where you will store your custom components.

Example:

...
.
└── WelcomeCard
    ├── WelcomeCard.tsx
    └── WelcomeCard.elements.ts

In this example WelcomeCardOverlay is a pure Functional component because chakra 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 things 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.

Rules:

For more information check Chakra UI - Style Props section.

Different component layouts

Sometimes, you might want to create a component specific for mobile and desktop.

In this case, inside a specific component, we could add a subfolder layouts (not to be confused with actual layout described below) to define how our component would look like on specific media query.

.
└── admin
    └── UserCard
        ├── layouts
        │   ├── UserCard.mobile.tsx
        │   └── UserCard.desktop.tsx
        └── UserCard.tsx

For this case, inside index.tsx we would have something like this (with help of Chakra UI Show/Hide):

import { Show, Hide } from '@chakra-ui/react';

export const UserCard = () => {
  return (
    <Hide above="md">
      <UserCardMobile />
    </Hide>
    <Show above="md">
      <UserCardDesktop />
    </Show>
  )
}

This is the case only for layouts that would add too much complexity when using standard css media queries.

Extracting utility functions and hooks

Sometimes, you will have complex functions or effects inside your component that will affect the readability of your component. In that case, you should extract them into separated files utils.ts and hooks.ts.

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.

Example:

.
├── ...
└── AlbumsCarousel
    ├── AlbumsCarousel.tsx
    └── AlbumsCarousel.utils.ts
    └── AlbumsCarousel.hooks.ts

Cluttered component

... imports ...

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 (
    //...
  )
}

Cleaned

... other imports ...
import { formatReleaseDate } from './utils';

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 (
    //...
    // formatReleaseDate used somewhere here
  )
}

Layouts

With Layouts, we generally define overall page structure that will be presented to the user based on a specific auth state or a specific page.

For example, your app could have an Admin dashboard on /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 will define your different layouts inside layouts folder:

.
.
└── components
    └── layouts
        ├── AdminLayout
        │   └── AdminLayout.tsx
        ├── MainLayout
        │   └── MainLayout.tsx
        └── BlogLayout
            └── BlogLayout.tsx

Then, when creating your routes (pages), you will 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>
  )
}

Utility components

Utility components are headless, which means that they don't have any impact on the UI itself. For example, Meta component for injecting meta tags inside document <head>.

Example:

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>
  );
};
src
.
└── components
    ├── shared
    │   ├── utilities
    │   │   └── Meta
    │   │       └── Meta.tsx
    ...

Setting up the store

Your datx store and models will be placed in the root of the src folder as follows:

src
├── models
│   ├── User.ts
│   └── Session.ts
└── store
    ├── utlis
    │   ├── config.ts
    │   └── initialize-store.ts
    └── index.ts

Fetchers

In src/fetchers you will organize your (swr) fetchers, usually by the model on which you will make API calls.

// src/fetchers/user.ts

import { AppCollection } from '../store';
import { User } from '../models/User';

export async function fetchUser(store: AppCollection, id: string): Promise<User> {
  try {
    const response = await store.fetch(User, id, {
      include: ['albums']
    });

    return response.data as User;
  } catch(response) {
    // handle response
  }
}

You will use your fetcher functions with useSwr hook inside of your components.

Setting up theming and styles

When creating styles for your core components, you will create the components folder inside of 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

When organizing test files, here are couple of quick rules:

For other in depth guides for testing take a look at the testing guide(needs update).

Folder structure would look something like this:

src
.
├── __mocks__
│   └── react-i18next.tsx
├── __tests__
│   ├── pages
│   │   └── user.test.ts
│   └── test-utils.tsx
├── pages
│   └── user.ts
├── fetchers
│   └── users
│       ├── users.ts
│       └── users.test.ts
└── components
    └── shared
        └── core
            └── Button
                ├── Button.test.tsx
                └── Button.tsx