ตั้งค่า ESLint และ Prettier ให้โปรเจกต์ Laravel ที่ใช้ Inertia กับ React

ESLint หาโค้ด JavaScript ที่น่าจะผิด ส่วน Prettier จัดหน้าตาโค้ด โพสต์นี้อธิบาย flat config ของ ESLint กฎของ React Hooks การเรียง class ของ Tailwind ด้วย prettier-plugin-tailwindcss และการตั้งให้สองตัวทำงานด้วยกันโดยไม่ตีกัน

เผยแพร่
เวลาอ่าน
4 นาที
ในโพสต์นี้ 5 หัวข้อ
  1. ESLint กับ Prettier แบ่งงานกันคนละเรื่อง
  2. Flat config คือไฟล์ eslint.config.js ไฟล์เดียว
  3. กฎของ React Hooks จับบั๊กที่หาเองยาก
  4. Prettier เรียง class ของ Tailwind ให้ด้วย
  5. รัน ESLint ก่อน แล้วปิดท้ายด้วย Prettier

พอโปรเจกต์ 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 17
  • files และ languageOptions บอกว่ากลุ่มกฎนี้ใช้กับไฟล์ไหน และโค้ดรันบน browser จึงรู้จัก window กับ document
  • react/prop-types: off ปิดกฎที่ให้ประกาศ propTypes ทุก component เพราะ props ของหน้า Inertia มาจาก controller ฝั่ง Laravel
  • head-key เป็น attribute ของ <Head> ใน Inertia ที่ใช้กันแท็กซ้ำ ใส่ไว้ไม่ให้ ESLint เตือนว่าไม่รู้จัก
  • prettier อยู่ท้ายสุดเสมอ จะได้ปิดกฎเรื่องรูปแบบที่ config ก่อนหน้าเปิดไว้

กฎของ React Hooks จับบั๊กที่หาเองยาก

eslint-plugin-react-hooks มีกฎสองข้อที่ใช้กันมานาน

  • rules-of-hooks hook ต้องเรียกที่ระดับบนสุดของ 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"
}

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 อยู่ในโพสต์ที่แยกเครื่องมือแต่ละแบบ

จบซีรีส์ เครื่องมือ Code Quality ใน Laravel แล้ว

ดูทั้ง 5 ตอน →

อ่านต่อ

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

ล่าสุด

ดูทั้งหมด →