Project Structure
The React code structure starts from the src folder. We can have the following files and folders in the src folder:
App.jsx
file
In a React application, the App.jsx
file typically serves as the main entry point or root component. This component encapsulates the overall structure of your application, acting as a container for other components that make up the user interface.
components
folder
This folder contains all the React components. Here are some points to consider when structuring the components
folder:
Logically group components into folders: We should logically group the components into folders according to the requirement.
In the case of smile-cart-frontend
, we have organized the components into folders such as Cart
, Checkout
, Product
, and ProductList
based on the pages in our application. Each folder can have an index.jsx file. The index file is considered the starting point of the folder.
Now, let's explore how component folders can be structured, taking the example of the Checkout
component. We broke down the Checkout
page UI into smaller UI elements and created components for each of them, keeping these components in the Checkout
folder. We then composed the Checkout
component with these smaller components by importing and rendering them according to the logic required in the index.jsx
file:
Rather than cluttering a single jsx
file with the entire UI, it's preferable to break down the functionality into specific components, each dedicated to a particular task, as depicted above.
As the application grows, we will have a deeply nested project structure with components used together grouped into folders.
Avoid creating unnecessary folders: A common mistake that should be avoided is creating folders for components when not required. For example, consider an arbitrary component called Dashboard
. If the Dashboard
component consists of only one file and is not composed of any other components, it should not be declared inside the components/Dashboard/index.jsx
file like this:
It should be declared inside the components/Dashboard.jsx
file.
Name components in PascalCase: All component files and folders should be named in PascalCase
, except for the index
file. The component files should have the .jsx
extension.
Place reusable components in commons
folder: All the reusable components should be stored in the commons
folder within the components
folder:
In addition to component files, it's common to include folders and files for constants, utils, hooks, and stores at various nested levels within the components folder. The following sections will delve into the structuring and use of these files and folders.
utils/constants
folder
As the name indicates, the constants
folder is used to store constants, and the utils
folder is used to store utility functions. Besides utility functions, Higher Order Components (HOC) are also placed under the utils
folder. The constants
and utils
folders can be placed at any nested level in the closest scope.
Here is an example from the smile-cart-frontend
repository, in which we store the project-level constants and utility functions in the constants
and utils
directly under the src
folder:
If we only have a single file for utility functions or constants, there's no need for separate folders. We can put the utility functions in a file called utils.js
and constants in constants.js
. If the utility functions or constants have any JSX, the filename should have a jsx
extension (utils.jsx
or constants.jsx
). You can see such constants.js
and utils.js
files at various nested levels within the smile-cart-frontend
repository.
Despite having only one file in the constants folder in the above example, we've kept the QUERY_KEYS
constants in a file named query.js
. This is a convention followed at BigBinary to maintain a separate file for QUERY_KEYS
for easier reference.
hooks
folder
This folder contains all the custom hooks. The hooks
folder can be placed at any nested level in the closest scope.
The custom hooks corresponding to the React query should be placed inside the src/hooks/reactQuery
folder:
stores
folder
This folder contains all the state management stores. In the case of smile-cart-frontend
, we only have the useCartItemsStore
:
The stores
folder can also be placed at any nested level in the closest scope.
lib
folder
We can have a lib
folder under the src
directory. This folder will contain all the files related to third-party libraries that we override or initialize.
apis
folder
All the backend calls are to be made from the src/apis
folder. We will have API connector files corresponding to each resource in this folder.
For example, we have a products.js
file to interact with the products
resource, a states.js
file to interact with the state
resource and so on:
translations
folder
This folder will contain all the translation related files. In smile-cart-frontend
, we have kept the translation strings corresponding to the English language in en.json
files in this directory:
routes.js
file
This file contains all the project-level routes.
Semantic folder and component names
While working on applications, the most basic yet confusing task is to name folders and components semantically correct. Many a times, we are unable to find an apt name for a component hence resorting to a vague name. However, this causes issues when someone else or you, yourself revisit the code after a few days.
When to add an index.js?
If there are multiple components exported from a namespace, then it's a good idea to add an index.js
within that namespace so that we don't have to add multiple imports but rather keep it down to a single import.
For example, we have following components exported from the commons
folder:
Without index.js
file, we required multiple imports for each component in the commons
folder as shown:
We have added an index.js
file in the commons
folder to reduce the number of import statements, like this:
Now after adding the above index.js
file we can import components using a single import statement, like this:
Default export vs named export
We should use the named exports only when we are exporting multiple entities from the same module as mentioned in the above section. In rest of the cases, we should use default exports.
For example, if there is only one file in components/commons
that is EmptyState.jsx
then there will be no index.js
file in that module as mentioned in the above section. This EmptyState.jsx
file is exporting only one component which is EmptyState
.
Now we can either make a named export or default export for the EmptyState
component, like this:
In the above-mentioned case, we should prefer the default export because there is only one export from this EmptyState.jsx
file.
We can import this EmptyState
component if it's a default export, like this:
Use a single import for a module
Take a look into the following piece of code:
So there are two imports, importing entities from the same module. This should be converted into a single import statement as it makes the code look more compact and removes unnecessary lines of code. Thus we should be writing the above imports like this:
If your project is using the eslint config from wheel, then eslint would automatically point out cases where we can use single import over multiple import statements.
Keep the files in closest scope
We don't need to keep the helper files or functions at global scope unnecessarily. We should always try to keep the files within the closest scope.
Consider the following folder structure:
Let's say we have a function called calculateCreatedAgo
in src/utils.js
which is used in CreateForm.jsx
and EditForm.jsx
files only. As the calculateCreatedAgo
function is used only within the Form
scope we should keep the calculateCreatedAgo
function in an utils.js
within the Form
folder.
The updated folder structure will be:
This doesn't mean that we shouldn't utilize the global src/utils
folder. If a particular utility is used across multiple scopes, then we should prefer to keep that utility function in the global scope that is in src/utils
folder. The main idea is to keep the things DRY as possible.
Keep useState at top
We should generally keep the useState
hook towards the top of the component. useState
is the most commonly used hook in the component, and this practice enhances code readability. All useState
hooks should be grouped together and placed at the top of the component. Following the useState
hooks, other hooks can be used, and custom variable definitions can follow them. Each group should be separated by a new line to provide clear visual segregation within the component, making it easier to find the required state or variable.
For example:
However, there can be exceptions where the initial value for a useState
hook depends on some other hook or context. In such cases, it's acceptable to use other hooks before the useState
hook.
For example, when using the useLocation
hook from a router library to initialize a state variable:
In this scenario, we use the useLocation
hook to determine the initial value for selectedTab
, and this exception improves the logic and maintainability of the component.
Another case is when utilizing a theme context to set the initial state based on the theme
In this theme context example, we use the useContext
hook to access the theme and then use it to determine the initial value for the backgroundColor
state variable.
Passing props the right way
We often pass boolean values as props to components. For example, disabled
, while specifying a disabled state for a button, we use the following syntax quite often -
The above code snipped can be written as -
Use semantic names for props
Always use camelCase for props.
Using spread operator in props
If you need to forward the props to inner component, you can list only the necessary props during destructuring and accept all the remaining props (to be forwarded) using spread operator.
A better way of writing the above case with more flexibility is as follows
Destructuring Props
The code snippet shown below does not destructure the props.
This not only shortens the code, but also improves IDE support. IDE can give you predictions and type support if you use destructuring in component or function definition.
Object destructuring can shorten the code when fetching data from an API call. Consider the following example:
Combining multiple states inside of an object
In the above example, we have used four states viz.,
This shortens code by eliminating the boilerplate associated with defining a new state.
But it also has its own down sides. The developer needs to consider the previous state when making changes to a property. i.e. the following code is error prone:
If both fetchTitle
and fetchCount
returns value at the same time, and both functions tries to update the state, only one of them succeeds. This is because setState
is not synchronous. The changes made by one of the functions will be overwritten by the other.
To fix the problem, we need to follow functional style for state updates:
We can also use point free expressions from ramda, to shorten the code:
Try to avoid unnecessary div
When there is a single component to be returned, we do not need to use div
Keep code DRY
Try to reduce duplication of code as much as possible using basic JavaScript techniques. Consider the following example
This can be simplified as:
Remove JavaScript code from JSX
As a good practice, keep the logic inside render to an absolute minimum.
Strings don't need curly braces
Use template literals
Avoid using string concatenation for strings and use template literals. It keeps the code clean.
Self closing tags
If a component does not have any children, then use self closing tags.
[Hack] Use object literals
Object literals can make our code more readable. There are cases when you can't use ternary operators and resort to either if-else
or switch
statements. In such cases, you could do the following
Note that these two aren't identical to each other. If the return values are function call results like this, they are evaluated only if it is the correct role
:
But if we write the object declaration style like this, all the functions will be executed at the time of object initialization:
If the functions are heavy weight or if the functions involves API calls, do not prefer this style.