Organizing components
UI Components
When adding UI components, you should be able to group them in three root domains:
1. Core Domain Core components are the smallest building blocks, highly reusable and composable. Examples include Card and Section.
2. Shared Domain Shared components are built out of core components and shared between feature components. Examples include InputField and MainLayout.
3. Features Domain Feature components are specific to a particular feature or section of the app. Note that the structure of the features folder may vary from project to project. Developers should evaluate if this pattern is suitable for their specific project requirements.
Folder naming rules:
kebab-case
folder name indicates domain namePascalCase
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 found 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:
UI
- higher order user interface componentsEntity
- UI representation of a data modelsUtility
- 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 , ... |
Primarily they should accept entity prop like this <UserCard user={user} /> where user is resource form the API, or in the rare occasions they could 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 a components folder, you are essentially breaking down larger main components into smaller, reusable chunks that are only relevant within that specific component.
Guidelines:
- Single Level Nesting: Only one level of component nesting should be used inside the components folder to keep the structure simple and maintainable.
- Component Purpose: These smaller components should not be reused outside their parent component.
- Naming Consideration: To avoid confusion with the root components folder, consider renaming this folder to elements. This renaming helps differentiate between global components and component-specific elements.
Example: For a MainTable component with a unique TableHeader, the structure should look like this:
// src/components/features/MainTable/components/TableHeader.tsx
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>
);
};
Folder structure for this feature:
src
└── components
└── features
└── MainTable
├── components
│ └── TableHeader.tsx
└── MainTable.tsx
Elements
If you have many style declarations inside your component file, making it difficult to read, create a separate file named `ComponentName.elements.ts to store your custom styled components.
Example:
└── WelcomeCard
├── WelcomeCard.tsx
└── WelcomeCard.elements.ts
WelcomeCard.tsx: ```js import { chakra } from '@chakra-ui/react';
export const WelcomeCardLayout = chakra('div', { baseStyle: { padding: '16px', borderRadius: '8px', boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', }, });
export const WelcomeCardTitle = chakra('h1', { baseStyle: { fontSize: '24px', color: 'teal', }, }); ```
WelcomeCard.elements.ts: ```js import { chakra } from '@chakra-ui/react';
export const WelcomeCardLayout = chakra('div', { baseStyle: { padding: '16px', borderRadius: '8px', boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)', }, });
export const WelcomeCardTitle = chakra('h1', { baseStyle: { fontSize: '24px', color: 'teal', }, }); ```
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 Creating Elements:
- Use .elements.tsx
for organizational purposes only.
- Custom components in .elements.tsx
should not be reused outside their root component.
- Combining chakra factory and functional components is allowed.
- Avoid using hooks inside elements.
For more information check Chakra UI - Style Props section.
Different component layouts for different screen sizes
In most of the cases we should strive 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.
But, 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>
)
}
layout
components
Different component layouts for different screen sizes in separate 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 a specific media query. We recommend using this approach only for 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 use this approach 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 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 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
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>
//...
)
}
Cleaned
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 (
//...
<div>{formatReleaseDate(props.startDate, props.endDate)</div>
//...
)
}
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
└── shared
└── 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
└── datx
└── create-client.ts
Fetchers
This section is considered deprecated. For the newer approach check the @datx/swr documentation.
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:
- Components, utils, fetchers... 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 pages folder. - All mocks should be placed in
__mocks__
folder
For other in depth guides for testing take a look at the testing guide.
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
└── core
└── Button
├── Button.test.tsx
└── Button.tsx
The complete structure
src
├── __mocks__
│ └── react-i18next.tsx
├── __tests__
│ ├── pages
│ │ └── user.test.ts
│ └── test-utils.tsx
├── components
│ ├── core
│ │ ├── Button
│ │ │ ├── Button.test.tsx
│ │ │ └── Button.tsx
│ │ ├── Section
│ │ │ ├── Section.test.tsx
│ │ │ └── Section.tsx
│ │ └── Card
│ │ ├── Card.test.tsx
│ │ └── Card.tsx
│ ├── features
│ │ ├── home
│ │ │ ├── HomeHeaderSection
│ │ │ │ ├── HomeHeaderSection.test.tsx
│ │ │ │ └── HomeHeaderSection.tsx
│ │ │ └── HomeTodoListSection
│ │ │ ├── HomeTodoListSection.test.tsx
│ │ │ └── HomeTodoListSection.tsx
│ │ ├── album
│ │ │ └── AlbumsCarousel
│ │ │ ├── AlbumsCarousel.tsx
│ │ │ ├── AlbumsCarousel.test.ts
│ │ │ ├── AlbumsCarousel.utils.ts
│ │ │ └── AlbumsCarousel.hooks.ts
│ │ └── todo
│ │ ├── TodoHeaderSection
│ │ │ ├── TodoHeaderSection.test.tsx
│ │ │ └── TodoHeaderSection.tsx
│ │ └── TodoCreateFormSection
│ │ ├── TodoCreateFormSection.test.tsx
│ │ └── TodoCreateFormSection.tsx
│ └── shared
│ ├── layouts
│ │ ├── AdminLayout
│ │ │ └── AdminLayout.tsx
│ │ ├── MainLayout
│ │ │ └── MainLayout.tsx
│ │ └── BlogLayout
│ │ └── BlogLayout.tsx
│ ├── fields
│ │ └── TextField
│ │ ├── TextField.test.tsx
│ │ └── TextField.tsx
│ ├── cards
│ │ ├── UserCard
│ │ │ ├── layouts
│ │ │ │ ├── UserCard.mobile.tsx
│ │ │ │ └── UserCard.desktop.tsx
│ │ │ ├── UserCard.test.tsx
│ │ │ └── UserCard.tsx
│ │ └── WelcomeCard
│ │ ├── WelcomeCard.test.tsx
│ │ ├── WelcomeCard.elements.ts
│ │ └── WelcomeCard.tsx
│ ├── todo
│ │ ├── TodoCard
│ │ │ ├── TodoCard.test.tsx
│ │ │ └── TodoCard.tsx
│ │ ├── TodoList
│ │ │ ├── TodoList.test.tsx
│ │ │ └── TodoList.tsx
│ │ └── TodoCreateForm
│ │ ├── TodoCreateForm.test.tsx
│ │ └── TodoCreateForm.tsx
│ └── utilities
│ ├── BugsnagErrorBoundary
│ │ └── BugsnagErrorBoundary.tsx
│ └── Meta
│ └── Meta.tsx
├── models
│ ├── User.ts
│ └── Session.ts
├── datx
│ └── create-client.ts
├── styles
│ └── theme
│ ├── index.ts
│ ├── styles.ts
│ ├── foundations
│ │ ├── font-sizes.ts
│ │ └── colors.ts
│ └── components
│ └── button.ts
└── pages
├── index.tsx
└── todo
└── [id]
└── index.tsx