DEV Community

Cover image for Modular Next.js Folder Strategy
Daniel Dallimore Mallaby
Daniel Dallimore Mallaby

Posted on

Modular Next.js Folder Strategy

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"],
    ...
}
Enter fullscreen mode Exit fullscreen mode

🔄 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
Enter fullscreen mode Exit fullscreen mode

🧩 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
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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};
  }
`;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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",
};
Enter fullscreen mode Exit fullscreen mode

💡 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;
Enter fullscreen mode Exit fullscreen mode

📌 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)