พอโปรเจกต์ Laravel ใช้ Inertia กับ React โค้ดอีกครึ่งหนึ่งย้ายไปอยู่ใน resources/js เป็นไฟล์ .jsx ที่ Pint, Rector และ PHPStan ไม่ได้อ่านเลย ฝั่งนี้มีเครื่องมือของตัวเองมาทำหน้าที่เดียวกัน คือ ESLint เป็น linter และ Prettier เป็น formatter
ถ้ายังสับสนว่า linter กับ formatter ต่างกันตรงไหน ลองอ่านโพสต์ที่แยกเครื่องมือแต่ละแบบก่อนได้ครับ
ESLint กับ Prettier แบ่งงานกันคนละเรื่อง
ESLint อ่านโค้ดทีละไฟล์ แล้วเทียบกับกฎที่เปิดไว้ เพื่อหาโค้ดที่น่าจะเป็นบั๊ก เช่น ตัวแปรที่ประกาศแล้วไม่ได้ใช้ หรือเรียก React hook ใน if บางกฎแก้ให้ได้ด้วย --fix แต่ส่วนใหญ่เราต้องอ่านแล้วแก้เอง
Prettier สนใจแค่หน้าตา เขียนโค้ดใหม่ทั้งไฟล์ตามความกว้างบรรทัด เครื่องหมายคำพูด และการย่อหน้าที่ตั้งไว้ ตัวเลือกมีน้อยโดยตั้งใจ ทีมจะได้ไม่ต้องเถียงกันเรื่องรูปแบบ
ESLint เคยมีกฎเรื่องรูปแบบโค้ดอยู่ด้วย ถ้าเปิดไว้พร้อมกับ Prettier สองตัวนี้จะแก้โค้ดกลับไปกลับมาไม่จบ eslint-config-prettier แก้ปัญหานี้ด้วยการปิดกฎของ ESLint ทุกข้อที่ทับกับงานของ Prettier ESLint จึงเหลือแค่งานหาบั๊ก
ติดตั้งทั้งหมดเป็น dev dependency
npm install -D eslint @eslint/js globals eslint-plugin-react eslint-plugin-react-hooks eslint-config-prettier prettier prettier-plugin-tailwindcss
Flat config คือไฟล์ eslint.config.js ไฟล์เดียว
ESLint รุ่นเก่าตั้งค่าในไฟล์ .eslintrc.* ที่ซ้อนกันได้หลายชั้น และมี .eslintignore แยกอีกไฟล์ ตั้งแต่ ESLint v10 ที่ออกเมื่อกุมภาพันธ์ 2026 ESLint ตัดรูปแบบเก่านี้ออกไปแล้ว เหลือแค่ flat config คือไฟล์ eslint.config.js ไฟล์เดียวที่ export array ของ config ออกมา ESLint ไล่อ่าน array จากบนลงล่าง ตัวที่อยู่ล่างกว่าทับค่าของตัวที่อยู่บน
ไฟล์ของเว็บนี้หน้าตาแบบนี้
import js from '@eslint/js';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
export default [
{
ignores: [
'public/**',
'bootstrap/ssr/**',
'resources/js/actions/**',
'resources/js/routes/**',
'resources/js/wayfinder/**',
'resources/js/vendor/**',
],
},
{ settings: { react: { version: 'detect' } } },
js.configs.recommended,
react.configs.flat.recommended,
react.configs.flat['jsx-runtime'],
{
files: ['resources/js/**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser },
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: { 'react-hooks': reactHooks },
rules: {
...reactHooks.configs.recommended.rules,
'react/prop-types': 'off',
'react/no-unknown-property': ['error', { ignore: ['head-key'] }],
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/refs': 'warn',
},
},
prettier,
];
ignoresข้ามไฟล์ที่ไม่ได้เขียนเอง ทั้งไฟล์ที่ build แล้ว SSR bundle และไฟล์ที่ Wayfinder generate ให้ในresources/js/actionsกับresources/js/routesไฟล์พวกนี้สร้างใหม่ทุกครั้งที่ build แก้ไปก็หายอยู่ดีjs.configs.recommendedกฎพื้นฐานของ JavaScript เช่น ตัวแปรที่ไม่ได้ใช้ หรือตัวแปรที่ไม่ได้ประกาศreact.configs.flat.recommendedกับjsx-runtimeกฎของ React โดยjsx-runtimeปิดกฎที่บังคับให้import Reactทุกไฟล์ ซึ่งไม่จำเป็นแล้วตั้งแต่ React 17filesและlanguageOptionsบอกว่ากลุ่มกฎนี้ใช้กับไฟล์ไหน และโค้ดรันบน browser จึงรู้จักwindowกับdocumentreact/prop-types: offปิดกฎที่ให้ประกาศpropTypesทุก component เพราะ props ของหน้า Inertia มาจาก controller ฝั่ง Laravelhead-keyเป็น attribute ของ<Head>ใน Inertia ที่ใช้กันแท็กซ้ำ ใส่ไว้ไม่ให้ ESLint เตือนว่าไม่รู้จักprettierอยู่ท้ายสุดเสมอ จะได้ปิดกฎเรื่องรูปแบบที่ config ก่อนหน้าเปิดไว้
กฎของ React Hooks จับบั๊กที่หาเองยาก
eslint-plugin-react-hooks มีกฎสองข้อที่ใช้กันมานาน
rules-of-hookshook ต้องเรียกที่ระดับบนสุดของ component เสมอ ห้ามอยู่ในif, loop หรือหลังreturnเพราะ React จำ hook ตามลำดับที่เรียกexhaustive-depsทุกค่าที่ใช้ในuseEffect,useMemoหรือuseCallbackต้องอยู่ใน dependency array ด้วย
ตัวอย่างที่เจอบ่อยในหน้า Inertia คือ effect ที่ใช้ prop แต่ลืมใส่ไว้ใน dependency
import { useEffect } from 'react';
import { router } from '@inertiajs/react';
export default function Posts({ tag }) {
useEffect(() => {
router.reload({ only: ['posts'], data: { tag } });
}, []);
return <h1>{tag}</h1>;
}
npx eslint resources/js/pages/Posts.jsx
resources/js/pages/Posts.jsx
7:8 warning React Hook useEffect has a missing dependency: 'tag'. Either include it or remove the dependency array react-hooks/exhaustive-deps
✖ 1 problem (0 errors, 1 warning)พอ tag เปลี่ยน effect นี้จะไม่รันซ้ำ หน้าจอจึงยังแสดงโพสต์ของ tag เดิม บั๊กแบบนี้มองด้วยตาแทบไม่เห็น แต่ ESLint ชี้ให้ทันที
plugin รุ่นใหม่ (เว็บนี้ใช้ 7.1) เพิ่มกฎใหม่เข้ามาในชุด recommended อีกหลายข้อ เช่น set-state-in-effect ที่เตือนเมื่อเรียก setState ตรง ๆ ใน effect และ refs ที่เตือนเมื่ออ่าน ref.current ระหว่าง render กฎใหม่เหล่านี้ตั้งเป็น error มาให้ โค้ดเดิมจึงอาจขึ้นแดงทีเดียวหลายจุด เว็บนี้ลดสองข้อนี้ลงเป็น warn ไว้ก่อน แล้วค่อยแก้ทีละไฟล์
Prettier เรียง class ของ Tailwind ให้ด้วย
ตั้งค่า Prettier ด้วยไฟล์ .prettierrc.json ส่วน .prettierignore ใส่ path ที่ไม่ต้องจัด แบบเดียวกับ ignores ของ ESLint
{
"tabWidth": 4,
"singleQuote": true,
"semi": true,
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./resources/css/app.css"
}
public/build
bootstrap/ssr
resources/js/actions
resources/js/routes
resources/js/wayfinder
resources/js/vendor
tabWidth: 4 ทำให้ไฟล์ JavaScript ย่อหน้าเท่ากับไฟล์ PHP ที่ Pint จัด เปิดสลับไปมาแล้วไม่ขัดตา
prettier-plugin-tailwindcss เรียง class ใน className ตามลำดับที่ Tailwind แนะนำ ลบช่องว่างเกินและ class ที่ซ้ำ ใครเปิดไฟล์ไหนก็เห็น layout ก่อน แล้วตามด้วยระยะห่าง สี และ state ในลำดับเดียวกัน plugin นี้ต้องอยู่ท้ายสุดของ plugins เสมอ
บน Tailwind v4 ค่า theme อยู่ในไฟล์ CSS ใน @theme ไม่ได้อยู่ใน tailwind.config.js แล้ว README ของ plugin จึงให้ตั้ง tailwindStylesheet ชี้ไปที่ไฟล์ CSS หลัก ถ้าไม่ได้ตั้ง plugin จะไม่รู้จัก class ที่เราสร้างเองใน @theme แล้วย้ายไปไว้หน้าสุด ลองดูผลจริงกับ text-seal ซึ่งเป็นสีที่เว็บนี้ตั้งเองใน @theme
// เขียนไว้
<div className="text-seal p-4 flex mt-2">
// ไม่ได้ตั้ง tailwindStylesheet: text-seal ไปอยู่หน้าสุด
<div className="text-seal mt-2 flex p-4">
// ตั้ง tailwindStylesheet แล้ว: text-seal เรียงอยู่กลุ่มเดียวกับ class สีอื่น
<div className="mt-2 flex p-4 text-seal">
รัน ESLint ก่อน แล้วปิดท้ายด้วย Prettier
ตั้งเป็น npm script ไว้ใน package.json แบบนี้
"scripts": {
"lint": "eslint resources/js",
"lint:fix": "eslint resources/js --fix",
"format": "prettier --write resources/js",
"format:check": "prettier --check resources/js"
}
ในเครื่องรัน lint:fix ก่อน แล้วจึง format เพราะ --fix ของ ESLint อาจเขียนโค้ดใหม่ แล้ว Prettier ค่อยจัดหน้าตาเป็นขั้นสุดท้าย
npm run lint:fix && npm run format
ใน pull request เว็บนี้รัน eslint กับ prettier --check เฉพาะไฟล์ .js และ .jsx ที่ PR แก้ ทั้งสองคำสั่งไม่แตะไฟล์ ถ้ายังมีจุดที่ไม่ผ่าน workflow จะขึ้นสีแดง คนเปิด PR ก็รันสองคำสั่งข้างบนในเครื่องแล้ว push ใหม่
ถ้าตั้ง editor ให้ Prettier จัดไฟล์ทุกครั้งที่บันทึก และให้แสดงคำเตือนของ ESLint ระหว่างพิมพ์ ส่วนใหญ่โค้ดจะผ่านตั้งแต่ก่อน commit แล้ว ทั้ง PhpStorm และ VS Code ตั้งได้ทั้งสองอย่าง
ถ้าใช้ Claude Code ช่วยเขียนโค้ด ตั้ง hook ให้รัน ESLint กับ Prettier ทุกครั้งที่ Claude แก้ไฟล์ได้ด้วย และถ้า ESLint ยังเจอปัญหาที่แก้เองไม่ได้ hook ก็ส่งคำเตือนกลับไปให้ Claude แก้ต่อ ตัวอย่าง hook อยู่ในโพสต์ที่แยกเครื่องมือแต่ละแบบ






