วิธีตั้งเกณฑ์ขนาดไฟล์และ component ในโปรเจกต์ React

ตั้งเกณฑ์เป็นตัวเลขสองระดับ คือเตือนกับควรแยก สำหรับความยาวไฟล์ ความยาว component จำนวน props จำนวน hook จำนวน state และจำนวน effect เพิ่มกฎเส้นแบ่ง server กับ client ที่ผิดเมื่อไหร่ก็ต้องแก้ แล้วให้ ESLint กับการตรวจไฟล์ที่ pull request แก้เป็นคนบังคับ โดยดูเฉพาะไฟล์ที่งานนี้ทำให้แย่ลง

เผยแพร่
เวลาอ่าน
6 นาที

หมวด 1 · ตั้งโปรเจกต์บทที่ 3 จาก 14

สารบัญบทเรียน บทที่ 3 จาก 14
ในบทนี้ 8 หัวข้อ
  1. ความรู้สึกว่ายาวไปใช้ตัดสินไม่ได้
  2. เกณฑ์สองระดับ
  3. กฎเส้นแบ่งที่ผิดเมื่อไหร่ก็ต้องแก้
  4. ให้เครื่องมือเป็นคนบังคับ
  5. ตรวจเฉพาะไฟล์ที่งานนี้ทำให้แย่ลง
  6. แยกก่อนเพิ่ม
  7. เมื่อไหร่ไม่ต้องเคร่งกับตัวเลข
  8. จากงานจริง

บทก่อนว่าด้วยการวางไฟล์ตาม feature คือไฟล์ใหม่ควรไปอยู่ที่ไหน บทนี้ตอบคำถามที่มาก่อนหน้านั้น คือเมื่อไหร่ถึงควรแยกไฟล์ออกมา ทุกบทในหมวด Component หลังจากนี้เริ่มจากตัวเลขในบทนี้

ความรู้สึกว่ายาวไปใช้ตัดสินไม่ได้

ทุกทีมมีไฟล์ที่ทุกคนรู้ว่ายาวไป แต่ไม่มีใครแยก เพราะไม่มีจุดไหนที่บอกว่าต้องแยกแล้ว ไฟล์ยาว 600 บรรทัดวันนี้ เดือนหน้ายาว 650 เพราะ feature ใหม่เพิ่มเข้าไปทีละนิด ไม่มี pull request ไหนที่ทำให้มันแย่ลงชัด ๆ จนวันหนึ่งไฟล์ยาวสองพันบรรทัด

ถ้าใช้ AI agent ช่วยเขียนโค้ด ปัญหานี้ยิ่งชัด ถามว่า "ไฟล์นี้ควรแยกไหม" สามครั้งก็ได้สามคำตอบ และ agent ไม่มีความรู้สึกรำคาญไฟล์ยาวแบบที่คนมี มันจะเพิ่มโค้ดลงไปในไฟล์ที่ยาวอยู่แล้วอย่างสบายใจ ถ้าไม่มีกฎบอกให้หยุด

ตัวเลขแก้ทั้งสองเรื่อง ทุกคนเห็นตรงกันว่าเกินหรือไม่เกิน และเครื่องมือตรวจให้ได้

เกณฑ์สองระดับ

เราใช้สองระดับ เตือน แปลว่าให้รู้ตัวว่าใกล้แล้ว ยังไม่ต้องทำอะไร ควรแยก แปลว่าถึงเวลาลงมือ ตัวเลขชุดนี้เป็นจุดเริ่มต้นที่ใช้ได้กับโปรเจกต์ React และ Next.js ส่วนใหญ่ ปรับได้ตามทีม

วัดอะไร เตือน ควรแยก ทำไม
บรรทัดของไฟล์ .tsx 250 400 เกินนี้ไฟล์มักทำหลายงาน
บรรทัดของไฟล์ .ts 400 600 logic อัดแน่นกว่า markup
จำนวน component ในไฟล์เดียว 3 5 component หนึ่งตัวควรทำงานเดียว
บรรทัดของ component ตัวเดียว 150 250 ตัว function ทำหลายงานอยู่
จำนวน props 8 12 สัญญาณว่าควรประกอบเป็นชิ้นแทน
จำนวน hook ที่เรียกใน component 8 12 แต่ละกลุ่มคือ custom hook ที่ยังไม่ได้ตั้งชื่อ
จำนวน useState / useReducer 5 8 มี state ที่คำนวณได้ซ่อนอยู่ หรือควรเป็น reducer
จำนวน effect 2 4 effect ส่วนใหญ่ควรเป็นค่าที่คำนวณหรือโค้ดใน event
บรรทัดของ custom hook 120 200 เกินนี้คือ hook สองตัว
จำนวน export ของไฟล์ 'use server' 12 20 ทุก export คือ endpoint

แต่ละแถวชี้ไปที่วิธีแก้ของมันเอง props เกินไปทางcompound component hook หรือ state เกินไปทางแยก custom hook effect เกินให้อ่านบทความ You Might Not Need an Effect ของ React ก่อน เพราะ effect ส่วนใหญ่ควรเป็นค่าที่คำนวณตอน render หรือโค้ดใน event handler

ความหนาแน่นสำคัญกว่าความยาว

ไฟล์ 300 บรรทัดที่มี component ห้าตัว แต่ละตัวมี state ของตัวเอง ควรแยกก่อนไฟล์ 500 บรรทัดที่เป็นแค่ตัวประกอบ ที่รับ props มาแล้ววางชิ้นส่วนลงใน JSX ตัวเลขความยาวไฟล์จึงเป็นแค่สัญญาณแรก ส่วนตัวเลขที่บอกว่าแยกตรงไหนคือจำนวน component, hook, state และ effect

ไฟล์ที่ยาวได้โดยไม่ผิด

บางไฟล์ยาวเพราะเป็นข้อมูล ไม่ใช่เพราะทำหลายงาน

  • ตาราง column ของ data table ที่มีสามสิบคอลัมน์
  • ไฟล์รวมตัวอย่างของ UI kit หรือ Storybook ที่มีหลายตัวอย่างต่อไฟล์
  • โค้ดที่ generate มา เช่น type จาก schema ของฐานข้อมูล
  • ไฟล์ test

ไฟล์พวกนี้ข้ามได้ แต่ให้เขียนเหตุผลไว้ ไม่ข้ามเงียบ ๆ ไม่อย่างนั้นวันหนึ่งไฟล์ component จริงจะถูกข้ามด้วยเหตุผลเดียวกัน

กฎเส้นแบ่งที่ผิดเมื่อไหร่ก็ต้องแก้

นอกจากขนาด มีสี่อย่างที่ผิดทุกครั้งไม่ว่าไฟล์จะยาวแค่ไหน ทั้งหมดเป็นเรื่องเส้นแบ่งระหว่างโค้ดฝั่ง server กับฝั่งเบราว์เซอร์ใน Next.js

  • ไฟล์ route ที่ใส่ 'use client' เช่น page.tsx หรือ layout.tsx ทั้งหน้ากลายเป็นโค้ดฝั่งเบราว์เซอร์
  • ไฟล์ฝั่ง client ที่ import โค้ดที่ควรอยู่บน server เช่น next/headers หรือ module ที่ต่อฐานข้อมูล
  • ไฟล์ .tsx ที่ import ฐานข้อมูลตรง ๆ การอ่านข้อมูลควรผ่าน data access layer
  • ฟังก์ชัน 'use server' ที่เขียนอยู่ในไฟล์ component

เหตุผลของแต่ละข้ออยู่ในหมวด Server และ Client ของคอร์สนี้ ที่ยกมาไว้ตรงนี้เพราะทั้งสี่ข้อตรวจด้วยเครื่องมือได้แน่นอน และควรตรวจไปพร้อมกับขนาด

ให้เครื่องมือเป็นคนบังคับ

ตัวเลขที่เขียนไว้ใน README อย่างเดียวไม่มีใครอ่าน ต้องมีอะไรสักอย่างที่บอกตอนเขียนโค้ด หรืออย่างน้อยตอนเปิด pull request

ESLint ตรวจได้เกินครึ่ง

eslint-config-next มี eslint-plugin-react มาให้แล้ว รวมกับกฎในตัวของ ESLint ก็ตรวจความยาวไฟล์ ความยาว function จำนวน component และ import ข้ามเส้นได้

import { defineConfig } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTs from 'eslint-config-next/typescript';

export default defineConfig([
  ...nextVitals,
  ...nextTs,
  {
    files: ['**/*.tsx'],
    rules: {
      'max-lines': ['warn', { max: 400, skipBlankLines: true, skipComments: true }],
      'max-lines-per-function': ['warn', { max: 250, skipBlankLines: true, skipComments: true }],
      'react/no-multi-comp': ['warn', { ignoreStateless: true }],
      'no-restricted-imports': ['error', {
        patterns: [{ group: ['@/lib/db', '@/lib/db/*'], message: 'อ่านข้อมูลผ่าน lib/data แทน' }],
      }],
    },
  },
  {
    files: ['**/*.ts'],
    rules: {
      'max-lines': ['warn', { max: 600, skipBlankLines: true, skipComments: true }],
    },
  },
  {
    files: ['**/*.test.{ts,tsx}', 'components/ui/examples/**'],
    rules: { 'max-lines': 'off', 'max-lines-per-function': 'off' },
  },
]);

ตัวเลขใน config เป็นระดับ "ควรแยก" ใส่เป็น warn ก่อน เพราะโปรเจกต์ที่มีอยู่แล้วจะมีไฟล์เกินเต็มไปหมด ถ้าใส่เป็น error ตั้งแต่วันแรก CI จะแดงทั้งโปรเจกต์ แล้วสุดท้ายก็มีคนปิดกฎทิ้ง ส่วนที่ทำให้ warn มีความหมายคือการตรวจเฉพาะไฟล์ที่งานนั้นแก้ ซึ่งอยู่ในหัวข้อถัดไป

ข้อห้ามโค้ดฝั่ง server ไหลเข้าเบราว์เซอร์ อีกชั้นที่ไม่ต้องตั้งอะไรเลยคือ package server-only ใส่ import 'server-only' ไว้บรรทัดแรกของไฟล์ที่อ่านฐานข้อมูล ถ้ามี component ฝั่ง client import มันมา build จะ error ทันที เอกสารของ Next.js แนะนำวิธีนี้ไว้เอง

ส่วนที่ ESLint นับให้ไม่ได้

ESLint ไม่มีกฎที่นับจำนวน props, จำนวน hook หรือจำนวน useState ใน component และไม่รู้ว่าไฟล์ไหนเป็นไฟล์ route ที่ใส่ 'use client' ไว้ ส่วนนี้มีสองทาง

  • เขียน script สั้น ๆ ด้วย TypeScript compiler API อ่านไฟล์เป็น AST (โครงสร้างของโค้ดที่ compiler เห็น) แล้วนับ props จาก type ของ parameter แรก นับการเรียกฟังก์ชันที่ชื่อขึ้นต้นด้วย use ใน component และดูบรรทัดแรกของไฟล์ route ทั้งหมดราวสองสามร้อยบรรทัด ถ้าใช้ AI agent ช่วยเขียนก็ใช้เวลาไม่นาน
  • ใส่ไว้ใน checklist ตอนรีวิว ถ้ายังไม่อยากมี script ให้คนรีวิวนับเองเฉพาะ component ที่ pull request นั้นแก้

ทางไหนก็ได้ ขอให้ตัวเลขเดียวกันถูกใช้ทุกครั้ง

ตรวจเฉพาะไฟล์ที่งานนี้ทำให้แย่ลง

ถ้ารันตรวจทั้งโปรเจกต์ครั้งแรก จะได้รายการไฟล์เกินเกณฑ์ยาวหลายร้อยไฟล์ รายการแบบนี้ไม่มีใครทำอะไรต่อ เพราะไม่ใช่ความผิดของ pull request ไหน ทางที่ใช้ได้จริงคือดูเฉพาะไฟล์ที่ pull request แก้ แล้วเทียบกับ version ตอนแตก branch (merge-base) แบ่งผลเป็นสามแบบ

ผล หมายความว่า ทำอะไรต่อ
ข้ามเส้น หรือไฟล์ใหม่ที่เกิน งานนี้ทำให้ไฟล์เกินเกณฑ์ แยกไฟล์ใน pull request ของตัวเองก่อน
ยาวขึ้นทั้งที่เกินอยู่แล้ว เกินมาก่อน และงานนี้ทำให้แย่ลงอีก ย้ายส่วนที่เพิ่มไปไว้ใน hook หรือ module ที่ดูแลเรื่องนั้น
เกินอยู่แล้ว แต่ไม่ได้ยาวขึ้น งานนี้ไม่ได้แตะส่วนที่เกิน ยังไม่ต้องทำอะไร

สองแบบแรกควรทำให้ CI ไม่ผ่าน แบบที่สามแค่รายงาน วิธีนี้ทำให้โปรเจกต์ดีขึ้นทีละนิดโดยไม่ต้องหยุดทุกอย่างมา refactor ทั้งโปรเจกต์ และไม่มีใครต้องรับผิดชอบไฟล์ที่ตัวเองไม่ได้ทำให้ยาว ถ้าใช้ GitHub Actions อยู่ การรัน ESLint เฉพาะไฟล์ที่ pull request แก้มีวิธีตั้งอยู่ในโพสต์เรื่อง lint เฉพาะไฟล์ที่เปลี่ยน

สำหรับการเลือกว่าจะแยกไฟล์ไหนก่อน ให้รันวัดทั้งโปรเจกต์แยกต่างหาก เรียงจากแย่ที่สุด แล้วหยิบไฟล์ที่ทั้งยาวและแก้บ่อยมาก่อน ไฟล์ที่ยาวแต่ไม่มีใครแตะมาสองปี รอได้

แยกก่อนเพิ่ม

กฎที่ใช้คู่กับตัวเลขทั้งหมด ถ้าจะเพิ่มโค้ดในไฟล์ที่เกินเกณฑ์ควรแยกอยู่แล้ว ให้แยกไฟล์ก่อน ใน commit หรือ pull request ของตัวเอง แล้วค่อยเพิ่มของใหม่ในไฟล์ที่สะอาดแล้ว

เหตุผลคือ diff ที่ทั้งย้ายโค้ดและแก้โค้ดในคราวเดียวรีวิวไม่ได้ คนรีวิวแยกไม่ออกว่าบรรทัดไหนแค่ย้ายมา บรรทัดไหนเปลี่ยนจริง และถ้ารอทำ feature เสร็จก่อนค่อยแยก การแยกนั้นจะไม่เกิดขึ้น เพราะพอ feature เสร็จก็มีงานถัดไปรออยู่เสมอ

กฎข้อนี้ใช้กับ AI agent ได้ดีมาก เขียนไว้ในไฟล์กฎของโปรเจกต์ (เช่น AGENTS.md หรือ CLAUDE.md) สั้น ๆ แบบนี้

## File size
Before adding code to a .ts/.tsx file that is already over a split threshold,
split it first in its own commit, then build on the split file.
A refactor commit only moves code: no behaviour, copy, export or signature changes.

เมื่อไหร่ไม่ต้องเคร่งกับตัวเลข

  • ตัวเลขเป็นสัญญาณ ไม่ใช่คำตัดสิน component 260 บรรทัดที่อ่านง่ายและไม่มีใครแก้บ่อย ไม่ต้องรีบแยกเพียงเพราะเกิน 250 ให้ใช้ระดับ "ควรแยก" เป็นจุดที่ต้องคุยกัน ไม่ใช่จุดที่ต้องแยกอัตโนมัติ
  • โปรเจกต์เล็กหรือ prototype ยังไม่ต้องมีเกณฑ์ ค่อยเพิ่มตอนที่เริ่มมีคนหลายคนหรือ agent หลายตัวแก้โค้ดพร้อมกัน
  • อย่าแยกเพื่อให้ตัวเลขผ่าน การตัด component ตรงกลางแบบสุ่มให้ได้สองไฟล์ที่ไฟล์ละ 200 บรรทัด แย่กว่าไฟล์เดียว 400 บรรทัด ให้แยกตามงานที่โค้ดทำ ซึ่งเป็นเรื่องของบทถัด ๆ ไป

จากงานจริง

ร้านค้าออนไลน์ Next.js 16 กับ CMS ที่เป็น Next.js admin ใน pnpm monorepo ตอนเริ่มวัดครั้งแรกมี 2,517 ไฟล์ เกินระดับ "ควรแยก" หรือผิดกฎเส้นแบ่งอยู่ 367 ไฟล์ ไฟล์ที่ยาวที่สุดเกือบ 2,500 บรรทัด เราไม่ได้ไล่แยกทั้งหมด แต่หยิบไฟล์ที่ยาวและแก้บ่อยที่สุดราว 70 ไฟล์มาแยกเป็นสี่รอบ หลังจากนั้นเหลือ 214 จาก 2,759 ไฟล์ จำนวนไฟล์เพิ่มขึ้นเพราะการแยกสร้างไฟล์ใหม่ ส่วนที่เหลือรอให้ pull request ที่แตะมันเป็นคนแยกตามกฎแยกก่อนเพิ่ม

ตัวเลขที่เห็นผลชัดที่สุดไม่ใช่ความยาวไฟล์ แต่เป็นจำนวน props ตารางข้อมูลตัวหนึ่งมี props 45 ตัว หลังจัดใหม่เหลือ 11 ตัว ส่วน component โครงหน้าของแอปจาก 23 เหลือ 8 ถ้าตั้งเกณฑ์ได้แค่ข้อเดียว เราจะเลือกจำนวน props ก่อน

บทถัดไป

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →