Organizing components
UI Components
When adding UI components, you should be able to group them in two root domains:
core
- primitives, low level componentsshared
- components that are shared all across the appfeatures
- root folder for components based on a specific feature (could be scoped by page or island)
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 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:
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 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:
- 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)
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:
- 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(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