How to Write a Component
Each component should be as generic as possible. Avoid creating components that are tightly coupled to specific business features or requirements. While you may initially build a component to address a particular business need, treat that as a starting point. After implementing the feature, refactor your code by extracting and splitting it into smaller, more focused components. Continue this process until you cannot extract or split the code any further.
These smaller components should be designed for maximum reusability, making them easy to use in different scenarios. This approach improves maintainability and scalability across your projects.
Write once, reuse everywhere.
Folder Structure
Every component should reside in its own folder. This folder should contain all code related to that component, including:
- The main component file
- Test files
- Styles (CSS/SCSS, etc.)
- Storybook files (if any)
- Helper files (with their own tests). If there are multiple helpers, group them in a
helpers
folder.
Helpers are typically needed at the feature level. This is because your components will often require input in a different data structure than what you receive from the API, and they may also output data in a format different from what your API expects. Therefore, you will likely need helper functions to transform data between these formats.
.
├── components/
│ ├── button/
│ │ ├── button.tsx // component
│ │ ├── button.css // styles
│ │ ├── button.test.tsx // unit tests
│ │ ├── button.stories.tsx // storybook documentation
│ │ └── index.ts // public API, exports button component
| |
│ └── modal/
│ ├── modal.tsx // component
│ ├── modal.css // styles
│ ├── modal.test.tsx // unit tests
│ ├── modal.stories.tsx // storybook documentation
│ └── index.ts // public API, exports modal component
|
└── features/
├── feedbackForm/
│ ├── feedbackForm.tsx // feature component
│ ├── feedbackForm.css // styles
│ ├── feedbackForm.test.tsx // integration tests
│ ├── feedbackForm.stories.tsx // storybook documentation
│ └── index.ts // public API, exports feedbackForm feature
|
└── feedbackPopup/
├── feedbackPopup.tsx // feature component
├── feedbackPopup.css // styles
├── feedbackPopup.test.tsx // integration tests
├── feedbackPopup.stories.tsx // storybook documentation
└── index.ts // public API, exports feedbackForm feature
Feature vs Component
Both are React components, but conceptually they are different.
Component
- Components can be thought of as the building blocks for features.
- Components are generic, their data structures, required props are not business requirement specific. Components should be re-usable in any given scenarios, to create any new business features without ever needing to change the components.
- Components should be closed for modification, no matter the requirement, components should never need to change. Read more about Open Close Principle
- Components have robust unit tests to ensure they work as expect in all the possible scenarios.
- You might think modal needs a gradient background header and submit/cancel/close buttons and you can configure them through props. No!, cause then it becomes a feature. Gradient background should be a separate component, buttons should be separate components and so on. Modal should just be a modal, that's it!
- Components should be small and focused, doing one thing only. Example: button, dropdown, modal, accordion, table, card etc.
- Components should not interact directly with browser storage; they should receive all necessary data via props.
- Components should not have margins, as this can limit their flexibility. The feature or parent should control layout and spacing.
Feature
- Features are composed using re-usable components as building blocks.
- A feature decides the arrangement and configuration of the re-usable components to meet the desired feature requirement.
- When the requirement changes, the arrangement and configuration changes, but the underlying re-usable components / building blocks do not change.
- Features have integration tests. While these are technically similar to unit tests, their purpose is to verify that the feature works as expected and all underlying components interact together correctly. For example, if your feature uses Redux, you do not test Redux itself, but rather ensure that the feature behaves correctly as a whole.
- API calls and state handling should be done inside the feature component, not outside the component. API handler function/hook can be defined outside the component but invocation should only happen within the component, respecting the component-lifecycle.
- Functions returned from hooks should not be passed as params to helper functions to be invoked outside the component. e.g.
const dispatch = useDispatch()
, here thedispatch
function should not be passed as param to helper functions to be called from outside the component.
- Helpers should be small, and pure functions organized in the
helper
folders.- Features can have a lot of logic code, to handle or process the data as per the business requirements, which should be split into several helper functions organized in the
helpers
folders. - Helper functions / utilities should be extracted as generic and re-usable functions and possibly organized outside the feature component as common utils / helpers.
- helper functions should be pure functions, i.e. they should not interact with anything external such as states or browser storage etc, everything a helper function needs should be passed as params to the function.
- Features can have a lot of logic code, to handle or process the data as per the business requirements, which should be split into several helper functions organized in the
After building a feature, refactor it by splitting out the UI and logic code into the smallest reusable pieces. Organize similar components and functions into folders as categories. And if there are several related functions, group them as class methods in a class.