Intro
Organizing your structure folder in Next.js is key to maintain a scalable and maintenable project, today i am gonna share one of the approach i use with Next.js page router.
⚙️ Part 0 - Next.js Configuration
This article use a custom folder that require you to change Next.js configuration, modify this line in the next.config.mjs
file
const nextConfig = {
pageExtensions: ["page.tsx"],
...
}
🔄 Part 1 - Reusable vs Dedicated Components
The first thing we want to do is divide our project between generic Reusable component and dedicated components.
A reusable component is a component that can be used anywhere (for example the ReloadButton.tsx
) while a dedicated component is a component that is relative so a specific page (for example the AboutMeHeader.tsx
or the AboutMeBody.tsx
)
├── components/
│ └── ReloadButton/
│ └── ReloadButton.tsx
└── pages/
└── about-me/
├── components/
│ ├── AboutMeHeader/
│ │ └── AboutMeHeader.tsx
│ └── AboutMeBody/
│ └── AboutMeBody.tsx
└── index.png
🧩 Part 2 - Component Structure Breakdown
For a better structure we want to divide a component into 5 parts: main, logic, style, configuration and types.
Let's see an example for a button that handle async requests
ReloadButton/
├── ReloadButton.tsx
├── ReloadButton.style.tsx
├── ReloadButton.conf.tsx
├── ReloadButton.d.tsx
└── useReloadButton.tsx
Main - ReloadButton.tsx
This main component serve as a connector for all the other components.
import React from "react";
import { Button } from "./ReloadButton.style";
import useReloadButton from "./useReloadButton";
import { ReloadButtonProps } from "./ReloadButton.d";
import { buttonConfig } from "./ReloadButton.conf.tsx";
const ReloadButton = ({ message, onClick }: ReloadButtonProps) => {
const { isReloading, handleReload } = useReloadButton({ onClick });
return (
<Button onClick={handleReload} disabled={isReloading}>
{isReloading ? buttonConfig.loadingText : message}
</Button>
);
};
Style - ReloadButton.style.tsx
The style component isolate the style of this component, it can contains the style of child components in order to isolate all the style of a single component in one single file.
We can expose some variable to ReloadButton.conf in order for them to be more easy to be configurated
import styled from "styled-components";
import { buttonConfig } from "./ReloadButton.conf";
export const Button = styled.button`
padding: 10px 20px;
background-color: ${buttonConfig.backgroundColor};
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
&:hover {
background-color: ${buttonConfig.hoverColor};
}
`;
Logic - useReloadButton.tsx
This logic component contain all the states and function of the ReloadButton
While it's possible for the useReloadButton to contain also the logic of child component it's important to note that if u have a useState inside your useReloadButton there will be two different state (one for each component with the useReloadButton), consider solutions with Context or Redux for sharing customHooks between components.
import { useState } from "react";
import { UseReloadButtonProps } from "./ReloadButton.d";
const useReloadButton = ({ onClick }: UseReloadButtonProps) => {
const [isReloading, setIsReloading] = useState(false);
const handleReload = async () => {
setIsReloading(true);
await onClick();
setIsReloading(false);
};
return {
isReloading,
handleReload,
};
};
export default useReloadButton;
Type - ReloadButton.d.tsx
Type is used to store all the types of the component
export interface ReloadButtonProps {
message: string;
onClick: () => Promise<void>;
}
export interface UseReloadButtonProps {
onClick: () => Promise<void>;
}
Config - ReloadButton.conf.tsx
Config is used to store all the constants and static configurations of the component
export const buttonConfig = {
loadingText: "Reloading...",
backgroundColor: "#0070f3",
hoverColor: "#005bb5",
};
💡 Part 3 - Example of usage
import React from "react";
import ReloadButton from "@/components/ReloadButton";
const App = () => {
const handleReloadClick = async () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("API call completed");
};
return (
<div>
<h1>Example App</h1>
<ReloadButton message="Reload Data" onClick={handleReloadClick} />
</div>
);
};
export default App;
📌 Part 4 - Conclusion
While this component could be considered overengineered, it's important to understand the structure in order to avoid having 2000 line files in bigger components!
One of the benefits of this solutions is improved search ,just search for your component name in vscode using CRTL + P or search for something like .style to search for all the styles around your application.
Thank you for reading this ,this was my first article and i hope you enjoyed :D
Top comments (0)