Skip to content

4.3 · State, styling, locale (พ.ศ.)

บทนี้เล่าว่า vtrc-web จัดการ state อย่างไร ใช้ระบบ CSS แบบไหน และทำไมการแสดงปีเป็น พ.ศ. ถึงกระจายอยู่หลายจุดในโค้ด

ถ้าผู้ใช้รายงานว่า "ปีที่แสดงผิด", "หน้านี้หน้าตาไม่เหมือนหน้าอื่น", หรือ "กดปุ่มแล้วข้อมูลเดิม ๆ ไม่อัปเดต" — บทนี้คือจุดเริ่มต้นของการสืบค้น

repo นี้เป็น React 16 class component เกือบทั้งหมด (250+ ไฟล์) ทำงานบน AntD v3 (antd: ^3.26.20 — ดู vtrc-web/package.json:20) และ moment.js (moment: ^2.29.4 — ดู vtrc-web/package.json:64) ทั้งสองเป็น dependency ที่ล็อกอยู่กับเวอร์ชันเก่าโดยไม่สามารถอัปเกรดง่าย ๆ ได้ ส่งผลต่อรูปแบบ state, styling, และ locale ตลอดทั้งบท


ภาพรวมก่อนลงรายละเอียด

มิติเทคโนโลยีที่ใช้สถานะ
State (component)this.state + this.setState (pure React)ไม่มี Redux/Recoil/MobX/Context
State (global)localStorage + sessionStorageไม่มี store กลาง
StylingAntD v3 default + compiled override + SCSS + utility + styled-componentsผสม 5 ชั้น
LocaleAntD LocaleProvider th_TH + moment.js th localeภาษาไทย + พ.ศ.
Date librarymoment.js 2.29 (locale th)heavy, ไม่ได้ย้ายไป dayjs/date-fns
Themehardcoded colors ใน CSSไม่มี design token, ไม่มี theme switcher
FontsKanit (primary), TH Sarabun (fallback)embed ใน repo

State management — local เท่านั้น ไม่มี global

vtrc-web ไม่มี Redux, Recoil, MobX, Zustand, Jotai, หรือ React Context API เพื่อจุดประสงค์ state sharing state ทั้งหมดในแอปอยู่ใน 3 ที่:

ที่ใช้เก็บอะไรscope
this.state ใน componentstate ของหน้า (loading, data, form values, modal flag)component
localStoragetoken, refreshToken, profile, noti counter, remember flag, encrypted credentialsข้าม tab + ข้ามวัน
sessionStoragetoken (ถ้าไม่ remember), tab-specific flagtab ปัจจุบัน

นอกจากนี้ยังมี state "กึ่งกลาง" อีก 1 ที่คือ URL param (this.props.match.params.<name> เช่น :postId, :rowID) — ไม่ใช่ state จริง แต่ใช้เป็น single source of truth ตอนเข้าหน้า detail

ไม่มี global loading

แต่ละ page จัด loading state ของตัวเองผ่าน this.state.loading หรือ this.state.preLoad — ไม่มี spinner กลาง ไม่มี NProgress ไม่มี Suspense ผู้ใช้ที่เข้าหน้าใหม่แล้วคลิกเร็ว ๆ อาจเจอหน้าว่างชั่วขณะเพราะแต่ละ page ต้อง fetch ข้อมูลของตัวเองใน componentDidMount

ไม่มี global error banner

error ทั้งหมดผ่าน toast (alertMessage(type, msg) ใน utils/message.js) หรือ message.error() ของ AntD โดยตรง — เป็น toast ที่หายไปเองใน 3 วินาที error ระดับ "หยุดใช้งาน" (เช่น token หมดอายุ) จะถูก errorLink ของ Apollo จับแล้ว redirect ไปหน้า Login โดยอัตโนมัติ

ผลกระทบของการไม่มี global state

ปัญหาวิธีแก้ในปัจจุบันข้อเสีย
profile ซ้ำซ้อนระหว่างหน้าquery meProfile ใหม่ในทุก page ที่ต้องการredundant network calls
noti counter อัปเดตช้าเก็บ counter ใน localStorage, polling ทุก น นาทีไม่ real-time
role flag กระจายlocalStorage.getItem('IsTrial') อ่านตรง ๆ ในหลาย pagesync ด้วยมือ
สลับหน้าแล้ว form state หายไม่เก็บ — ถามว่า "ออกจากหน้าไหม" ก่อน history.pushUX ไม่ราบรื่น

ถ้าวันหน้าทีมอยากใส่ global state ตัวเลือกที่เข้ากับ pattern เดิมที่สุดคือ React Context API (built-in, ไม่ต้อง dependency) — แต่ต้องครอบใน Root component ที่ src/index.js:80

localStorage / sessionStorage — key ที่ใช้บ่อย

keyที่เก็บตัวอย่างค่า
tokenJWT ปัจจุบันeyJhbGc...
refreshTokenrefresh tokeneyJhbGc...
isRememberflag remember me'true'
appUsernameusername ที่ login'sso-12345'
sessionIDsession idUUID
lsidlogin session idUUID
organizationorg id'3F3BF3AD-...'
IsTrialflag ทดลองใช้'true'
objectValuePFprofile object cacheJSON string
objectValueEMemployee object cacheJSON string
profileDetailPWencrypted passwordAES encrypted
profileDetailUNencrypted usernameAES encrypted
expiredDateวันที่ password จะหมดอายุISO date
localtokenclient id (random)UUID
reCaptchaCheckflag captcha''

helpers สำหรับจัดการ storage

helperที่มาหน้าที่
setToken({accessToken, refreshToken}, isRemember)vtrc-web/src/utils/auth.js:4เก็บ token ตาม isRemember (เลือก localStorage หรือ sessionStorage)
getToken()vtrc-web/src/utils/auth.js:23อ่าน token + refreshToken (เลือก storage ที่ถูก)
clearAllStorage()vtrc-web/src/utils/auth.js:99logout — ล้างทั้งสอง storage
setWithExpiry(key, value, ttl)vtrc-web/src/utils/storage.jsเก็บ key พร้อม expiry (ms)
getWithExpiry(key)vtrc-web/src/utils/storage.jsอ่าน key + return null ถ้าหมดอายุ

กฎเหล็กเรื่อง state

  1. ห้ามอ่าน localStorage.getItem('token') ตรง ๆ — ใช้ getToken() เสมอ เพราะต้องเลือก localStorage หรือ sessionStorage ตาม flag isRemember
  2. ห้ามเก็บ object ใน storage โดยไม่ JSON.stringifylocalStorage เก็บ string เท่านั้น
  3. ห้าม sync state ระหว่าง component ผ่าน global variable (เช่น window.foo = ...) — ใช้ props หรือ query ใหม่
  4. ถ้า state ใหญ่เกินไปใน component เดียว ให้แบ่งเป็น sub-component แล้วส่ง props ลงไป อย่าใช้ state library มาช่วย

ตัวอย่าง pattern ที่ใช้จริง

javascript
class LeaveFormPage extends Component {
  state = {
    loading: false,
    leaveType: null,
    startDate: null,
    endDate: null,
    modalVisible: false,
  }

  componentDidMount() {
    this.fetchInitialData()
  }

  fetchInitialData = async () => {
    this.setState({ loading: true })
    const { data, errors } = await this.props.client.query({
      query: Queries.fetchLeaveTypeList,
      variables: { organization: localStorage.getItem('organization') },
    })
    if (errors) {
      this.setState({ loading: false })
      return
    }
    this.setState({ leaveType: data.fetchLeaveTypeList, loading: false })
  }

  handleSubmit = () => {
    this.setState({ modalVisible: true })
  }
}

สังเกตว่า loading, leaveType, modalVisible อยู่ใน this.state ทั้งหมด — component นี้ไม่รู้ว่ามี component อื่นอยู่ในหน้าไหม และไม่สนใจ ถ้าอีกหน้าต้องการข้อมูลเดียวกันต้อง fetch ใหม่


Styling — ผสม 5 ชั้น

vtrc-web ใช้ CSS ผสมจาก 5 แหล่ง ทำให้บางครั้ง style ทับซ้อนกันและ debug ได้ยาก

ชั้นที่ 1 — AntD v3 default

AntD v3 import แบบ full (ไม่ใช่ tree-shake) — ทุก component ใน library ถูกโหลด CSS มา import 'antd/dist/antd.css' อยู่ในไฟล์ entry (ส่วนใหญ่ใน component ที่ใช้ครั้งแรก) เป็น base style ของทุก component

เวอร์ชัน antd: ^3.26.20 (vtrc-web/package.json:20) เป็นเวอร์ชันสุดท้ายของ v3 ก่อน v4 ออก — ทีมยังอยู่บน v3 เพราะ v4 เปลี่ยน API ของ Form จาก HOC Form.create() + getFieldDecorator เป็น hooks Form.useForm() + Form.Item name= ซึ่งกระทบ 100+ page ใน repo

ชั้นที่ 2 — AntD override compiled (ใหญ่ที่สุดใน repo)

vtrc-web/src/config/custom-antd.css คือไฟล์ compiled ที่ใหญ่ที่สุดใน repo (21,190 บรรทัด) — override style ของ AntD v3 แบบเต็ม ไม่ใช่ source แต่เป็น output จากการ compile .less ของ AntD แล้ว modify ทับ

css
/* บรรทัด 1-5: stylelint disable flags — บอกว่าไฟล์นี้ generated */
/* stylelint-disable at-rule-empty-line-before,at-rule-name-space-after,at-rule-no-unknown */
/* stylelint-disable no-duplicate-selectors */
/* stylelint-disable */
/* stylelint-disable declaration-bang-space-before,no-duplicate-selectors,string-no-newline */
/* stylelint-disable at-rule-no-unknown */
html,
body {
  width: 100%;
  height: 100%;
}

ส่วน reset และ base อยู่บรรทัดแรก ๆ ของ vtrc-web/src/config/custom-antd.css:6-100 ครอบทั้ง box-sizing, font-family, hr, abbr, address แบบ normalize.css เหมือน AntD ปกติ ที่เหลืออีก 21,000+ บรรทัดคือ override ของ component แต่ละตัว (.ant-btn, .ant-input, .ant-table, .ant-modal, ...)

ห้ามแก้ custom-antd.css โดยตรง — ไฟล์ใหญ่เกินไป และเป็น output จาก build ถ้าจะ override ให้เพิ่ม className ของตัวเองแล้วเขียน CSS ใน main.css หรือ styles.css

ชั้นที่ 3 — global SCSS (compile ด้วย Prepros)

source คือ vtrc-web/src/assets/sass/main.scss (1,764 บรรทัด) คอมไพล์ด้วยเครื่องมือ Prepros (manual — ไม่ได้อยู่ใน webpack pipeline) ออกมาเป็น vtrc-web/src/assets/css/main.css (4,565 บรรทัด)

css
html {
  position: relative;
  height: 100%;
}

body {
  font-size: 16px;
  color: rgba(0, 0, 0, 0.65);
  font-family: "Kanit-Medium";
}

import ครั้งเดียวใน entry ใช้ได้ทุกหน้า

ไฟล์ใน vtrc-web/src/assets/css/ มี 4 ไฟล์:

ไฟล์บทบาท
main.cssoutput จาก sass/main.scss — global style หลัก
main_bk.cssbackup ของ main.css (อย่าแก้)
app.cssutility เล็ก ๆ สำหรับ layout บางส่วน
variables.cssCSS custom properties (ใช้น้อย)

ชั้นที่ 4 — utility CSS สั้น ๆ

vtrc-web/src/config/styles.css (128 บรรทัด) — utility class เล็ก ๆ เช่น .card-bg-green, .btn-white-red, .color-red ใช้สำหรับกรณีที่ไม่อยากเขียน CSS ใหม่

ชั้นที่ 5 — styled-components (ใช้น้อย)

บางหน้าใช้ styled-components (มีใน vtrc-web/package.json:100 เป็น ^5.3.5) สำหรับ scoped style ส่วนใหญ่ในหน้า Login:

javascript
const BackgroundDiv = styled.div`
  display: flex;
  justify-content: center;
  align-items: center;
`

const FormDiv = styled.div`
  padding: 50px 100px;
  box-shadow: 30px 40px 40px 1px rgba(0, 0, 0, 0.15), 0 0px 0px rgba(0, 0, 0, 0.25);
  border-radius: 20px;
  background-color: #fff;
`

การใช้ styled-components ใน repo นี้จำกัดอยู่ที่หน้า Login และอีก 2-3 หน้า — ไม่ใช่ pattern หลัก ถ้าเห็นในหน้าอื่นให้ถือว่าเป็น code ทดลอง

ลำดับความสำคัญของ style (cascade order)

จากซ้ำไปหนา:

  1. AntD default.ant-btn { ... } จาก antd.css
  2. custom-antd.css — compiled override 21,190 บรรทัด
  3. main.css — global SCSS ที่ compile แล้ว
  4. styles.css — utility class
  5. styled-components inline — เฉพาะ component ที่ใช้

กฎเหล็กเรื่อง styling

  • ถ้าจะ override AntD component ให้ใช้ className ของตัวเอง + target ใน main.css หรือ styles.css — ห้ามแก้ custom-antd.css
  • ระวัง specificity — AntD override มักใช้ !important บ่อย ทำให้ override ของเราไม่ทำงาน
  • ระวัง load order — import order ใน entry มีผลต่อ cascade ให้ตรวจ index.js และ App/index.js ทุกครั้งที่ style แปลก ๆ
  • ห้ามเพิ่ม Tailwind, CSS Modules, หรือ utility-first framework ใหม่ — ผิด pattern ทั้งระบบ

OS / browser-specific class

vtrc-web/src/index.js:46-76 มี script สำหรับ detect OS และ browser แล้วเพิ่ม class ใน <html>:

javascript
const ua = navigator.userAgent || "";
const platform = navigator.platform || "";

const isWindows = /Win(dows)?/.test(platform) || /Windows/.test(ua);
const isMac = /Mac/.test(platform) || /Macintosh/.test(ua) || /Mac OS/.test(ua);
const isChromium = /Chrome\/\d+/.test(ua)
  && !/Edg\/\d+/.test(ua)
  && !/OPR\/\d+/.test(ua)
  && !/Brave\//.test(ua);

if (isWindows) document.documentElement.classList.add("os-windows");
if (isMac) document.documentElement.classList.add("os-macos");
if (isChromium) document.documentElement.classList.add("browser-chrome");
if (isWindows && isChromium) document.documentElement.classList.add("win-chrome");
if (isMac && isChromium) document.documentElement.classList.add("mac-chrome");

class เหล่านี้ใช้ใน CSS เพื่อทำ workaround เฉพาะ environment เช่น .win-chrome .ant-modal { ... } — ถ้าเจอ CSS rule แปลก ๆ ที่ target .os-windows หรือ .win-chrome ให้รู้ว่าเป็น bug fix เฉพาะตัว

fonts

vtrc-web/src/assets/fonts/
├── Kanit-Medium.*       (primary body font)
├── Kanit-Regular.*
├── THSarabunPSK.*       (alternative Thai font)
└── ...

ค่า default คือ "Kanit-Medium" — ใช้ Kanit เป็นหลัก ไม่มี @font-face import แบบ dynamic ทุกไฟล์ embed ใน repo ผ่าน @font-face ใน main.scss


Locale — AntD LocaleProvider + moment.js

vtrc-web/src/index.js:7-8 import AntD v3 LocaleProvider และ locale pack ภาษาไทย:

javascript
import { LocaleProvider } from 'antd';
import th_TH from 'antd/lib/locale/provider/th_TH';

และครอบ <App /> ใน Root component (vtrc-web/src/index.js:80-90):

javascript
class Root extends Component {
  render() {
    return (
      <ApolloProvider client={apolloClient}>
        <LocaleProvider locale={th_TH}>
          <App />
        </LocaleProvider>
      </ApolloProvider>
    );
  }
}

ผล: AntD component ทุกตัว (DatePicker, Pagination, Table, Modal, Popconfirm) แสดงภาษาไทยโดยอัตโนมัติ รวมถึงปุ่ม "OK / Cancel" ของ Popconfirm ที่กลายเป็น "ตกลง / ยกเลิก"

หมายเหตุ: AntD v4+ เปลี่ยนชื่อ LocaleProvider เป็น ConfigProvider — ถ้าวันหน้าอัปเกรดต้องแก้ที่นี่

moment.js locale ไทย

vtrc-web/src/index.js:9-12 import moment.js และ locale ไทย:

javascript
import moment from 'moment';
import 'moment/locale/th';

moment.locale('th');

moment.js locale th ทำ 2 อย่างอัตโนมัติ:

  1. แปลงชื่อเดือนเป็นไทยmoment().format('MMMM') ได้ กรกฎาคม แทน July
  2. เปลี่ยน year offsetmoment().format('YYYY') ยังได้ C.E. แต่ moment().format('LL') ได้ 10 กรกฎาคม 2569 (พ.ศ.)

moment.updateLocale override

vtrc-web/src/index.js:36-45 มีการปรับแต่ง locale ไทยเพิ่มเติม:

javascript
moment.updateLocale('th', {
  longDateFormat: {
    LT: 'HH:mm',
    LTS: 'HH:mm:ss',
    L: 'DD/MM/YYYY',
    LL: 'D MMMM YYYY',
    LLL: 'D MMMM YYYY เวลา HH:mm',
    LLLL: 'วันddddที่ D MMMM YYYY เวลา HH:mm'
  }
});

ผล: moment().format('LLLL') ได้ วันจันทร์ที่ 13 กรกฎาคม 2569 เวลา 22:47 — เป็นรูปแบบที่อ่านง่ายสำหรับ UI ไทย

โอเวอร์ไรด์ moment.format (ถูกปิดไว้)

vtrc-web/src/index.js:14-33 มีโค้ด override moment.fn.format และ moment.fn.year แบบ global ที่ถูก comment ไว้ทั้งหมด:

javascript
// เก็บฟังก์ชัน format เดิม
const _format = moment.fn.format;

// override format ของ moment ให้แสดงปีเป็น พ.ศ.
// moment.fn.format = function (f) {
//   if (!f) return _format.call(this);
//   return _format.call(this, f.replace(/YYYY/g, this.year() + 543));
// };

// const _year = moment.fn.year;

// override year ให้เป็น พ.ศ.
// moment.fn.year = function (y) {
//   if (typeof y === 'undefined') {
//     return _year.call(this) + 543;
//   }
//   return _year.call(this, y - 543);
// };

การ override global prototype เป็นอันตรายเพราะไลบรารีอื่น (AntD DatePicker, react-pdf, ฯลฯ) อาจพัง ทีมจึง comment ไว้ แล้วใช้ helper function แทน

helper function สำหรับแปลง พ.ศ.

vtrc-web/src/utils/formatdate.js (442 บรรทัด) มี helper 22+ ตัว ส่วนใหญ่ใช้ manual +543:

javascript
export const changeYear = (year) => {
  return year + 543;
};

และ helper ที่จัดรูปแบบวันที่ภาษาไทย + ปี พ.ศ.:

javascript
export const changeFormatdate = (date) => {
  let monthThai = [];
  monthThai["01"] = "มกราคม";
  monthThai["02"] = "กุมภาพันธ์";
  // ... 12 เดือน
  if (date) {
    const newdate =
      moment(date).format("D") +
      " " +
      monthThai[moment(date).format("MM")] +
      " " +
      (parseInt(moment(date).format("YYYY")) + 543).toString();
    return newdate;
  }
};

ตาราง helper ที่ใช้บ่อย

helperรูปแบบ outputใช้ใน
changeFormatdate(date)10 กรกฎาคม 2569หน้าส่วนใหญ่
changeFormatdateSlip(date)10 กรกฎาคม 2569 (UTC→local ก่อน)Slip
changeFormatFullDateTimeThai(date)10 กรกฎาคม 2569 เวลา 14:30detail ต่าง ๆ
changeFormatMonthYearThai(date)ตุลาคม 2569report
changeFormatdateThai(date)เหมือน changeFormatdateleave
changeOnlyMonthSlip(date)กรกฎาคม (เดือนอย่างเดียว)Slip dropdown
changeYear(year)2569 (เลขปีอย่างเดียว)คำนวณ
pushAge(birthDate)อายุเป็นปีprofile
pushAgeWork(startDate)อายุงานเป็นปีprofile
pushAgeDate(birthDate, deathDate)อายุโดยระบุวันสิ้นสุดกรณีเสียชีวิต
changeFormatdateStartEnd(start, end)10 ก.ค. 2569 - 15 ก.ค. 2569leave list
dateFormatNumber(values)25691010ใช้ใน query
disabledDate(current)disable วันที่ผ่านไปแล้วDatePicker

กฎเหล็กเรื่อง พ.ศ.

  1. ห้าม +543 ตรง ๆ ใน component — ใช้ helper changeFormatdate() หรือ changeYear() เสมอ
  2. ห้ามใช้ moment().year() ตรง ๆ เพื่อแสดงผล — จะได้ C.E.
  3. ถ้าจะแสดงปีเป็น พ.ศ. ใน dropdown — ใช้ changeYear(year) ก่อน set เป็น options
  4. moment.js locale th จะ format ให้เป็น พ.ศ. อัตโนมัติเมื่อใช้ .format('LL') หรือ .format('LLLL') — แต่ .format('YYYY') ยังได้ C.E.
  5. ถ้าข้อมูลจาก server ส่ง พ.ศ. มาแล้ว ห้าม +543 ซ้ำ — ตรวจก่อนเสมอ

ความซ้ำซ้อนใน helper

helper หลายตัว duplicate logic (mapping เดือน 12 ตัวเขียนซ้ำในแต่ละ function) — ถ้าจะ refactor แนะนำให้สร้าง constant MONTH_TH กลาง แล้วให้ทุก helper import ใช้

ปัจจุบัน monthThai array ถูกประกาศซ้ำใน 7+ function ใน formatdate.js:

javascript
let monthThai = [];
monthThai["01"] = "มกราคม";
monthThai["02"] = "กุมภาพันธ์";
// ...

ความขัดแย้งระหว่าง AntD และ moment locale

AntD v3 DatePicker ใช้ moment.js ภายใน — พอเรา moment.locale('th') DatePicker จะแสดงชื่อเดือนเป็นไทยโดยอัตโนมัติ แต่ปีใน picker ยังเป็น C.E. (เพราะ UI ของ picker เองไม่ได้เรียก .format('YYYY'))

ผล: ผู้ใช้เลือกปี "2026" ใน picker แต่พอ confirm แล้วเรา format ด้วย helper จะได้ "2569" — บางครั้งทำให้ผู้ใช้สับสน

ถ้าจะทำ picker แสดง พ.ศ. จริง ต้อง custom dateRender ของ DatePicker เอง — แต่ใน repo ไม่ได้ทำ


Theme — hardcoded ไม่มี design token

repo ไม่มี theme system ไม่มี design token, ไม่มี CSS variables ที่ใช้งานจริง (vtrc-web/src/assets/css/variables.css มีอยู่แต่ใช้น้อยมาก)

จุดค่า
primary color#1890ff (AntD default)
success color#52c41a (AntD default)
error color#f5222d (AntD default)
text colorrgba(0, 0, 0, 0.65) (main.scss)
font size16px (body)
font family"Kanit-Medium"

ถ้าจะเปลี่ยน primary color ต้องแก้ใน custom-antd.css (ซึ่งเป็นไฟล์ compiled) หรือ override .ant-btn-primary { background: #... } ใน main.css — ทั้งสองวิธีไม่สะอาด


Axios instances — หลายตัวสำหรับ REST services

นอกจาก Apollo (GraphQL) vtrc-web ยังใช้ axios สำหรับ REST services หลายตัว (axios: ^0.21.4vtrc-web/package.json:29)

1. axios-bo.js — instance หลักพร้อม interceptor

javascript
export const AxiosBO = (setHeader = true) => {
   const defaultOptions = {}
    if (setHeader === true) {
        defaultOptions.headers = {
            "Content-Type": "application/json"
        }
    }

    const instance = axios.create(defaultOptions);

    instance.interceptors.request.use(function (config) {
        const { token } = getToken()
        config.headers.authorization = token ? `Bearer ${token}` : "";
         if(!config.url.includes("/meeting/secure/")){
            config.headers.apiKey = atob(apiKeyNews());
         }
        return config;
    });

มี interceptor สำหรับ refresh token เมื่อได้ 401 (บรรทัด 45-83 ของ axios-bo.js) — เหมือน errorLink ของ Apollo แต่สำหรับ REST

2. axiosAPI.js — ใช้ใน Login (SSO)

ใช้สำหรับ ${checkLinkURL()}SSOService/checkEmpLogin ในหน้า Login

3. axios-i3.js — ใช้สำหรับ I3 services

ใช้ในบางหน้าสำหรับ I3 internal services

ข้อควรระวังเกี่ยวกับ axios

  • แต่ละ instance มี interceptor ต่างกัน — ถ้าจะ refresh token ต้องเลือก instance ที่มี interceptor ครบ
  • AxiosBO() เป็น factory function — ทุกครั้งที่เรียกสร้าง instance ใหม่ ทำให้มี overhead ถ้าเรียกบ่อย
  • มีเงื่อนไขพิเศษสำหรับ URL ที่มี /meeting/secure/ — ไม่แนบ apiKey ใน request

Console override — ปิด console ทั้งระบบ

มี global side effect ที่ทำให้ debug ฝั่ง production ได้ยาก:

javascript
console.log = console.warn = console.error = () => { };

ทุก console.* ถูกทำให้เป็น no-op ในโหลด production ถ้าจะ debug ใน browser ต้อง comment บรรทัดนี้ชั่วคราว หรือใช้ debugger statement + DevTools


PWA — service worker unregister

vtrc-web/src/index.js:93 เรียก serviceWorker.unregister() — webpack ยังสร้าง SW ไว้ (ผ่าน workbox-webpack-plugin: 4.3.1vtrc-web/package.json:109) แต่ฝั่งแอปไม่ register จึงไม่ทำงาน

ถ้าจะเปิด PWA จริง ต้องเปลี่ยนเป็น serviceWorker.register() และออกแบบ cache invalidation


dangerouslySetInnerHTML — ใช้หลายที่

grep พบการใช้ dangerouslySetInnerHTML ใน 20+ ไฟล์ — ส่วนใหญ่เพื่อ render markdown content หรือ HTML ที่ได้จาก server

ไฟล์ใช้ทำอะไร
Slip/index.jsrender text payroll slip (HTML)
Notification/index.jsrender ข้อความ notification
News/index.js, DetailNews/index.jsrender เนื้อหาข่าว
Policy/index.js, PolicyDetail/index.jsrender policy text
PmsIndicators/form/...render modal content
Orientation/index.jsrender orientation content

ความเสี่ยง

ถ้า content มาจาก user input โดยไม่ sanitize → XSS risk

helper markdown

vtrc-web/src/utils/markDown.js (16 บรรทัด) แปลง markdown → HTML ผ่าน draftjs — แต่ใช้น้อย ส่วนใหญ่ใช้ react-markdown (react-markdown: ^8.0.3vtrc-web/package.json:88) ในหน้า Slip


เคล็ดลับตอนทำงานจริง

ตอน debug "ปีที่แสดงผิด"

  1. เช็คว่าใช้ helper อะไร (changeFormatdate vs moment().format('YYYY'))
  2. เช็คว่าข้อมูลจาก server เป็น C.E. หรือ พ.ศ. แล้ว — ถ้า server ส่ง พ.ศ. มาแล้ว ห้าม +543 ซ้ำ
  3. เช็ค locale ของ moment — .locale() ต้องเป็น 'th'

ตอน debug "style ไม่ทำงาน"

  1. ตรวจ className ใน DevTools → ดูว่ามี CSS rule ไหน match
  2. ตรวจ specificity — AntD override มักจะ !important บ่อย
  3. ตรวจ load order ใน index.jsmain.css โหลดก่อน custom-antd.css หรือไม่
  4. ตรวจ OS/browser class — บาง rule ทำงานเฉพาะ .win-chrome หรือ .os-macos

ตอนจะเก็บข้อมูลใน localStorage

  1. ใช้ helper setWithExpiry ถ้าต้องการ expiry
  2. JSON.stringify object ทุกครั้ง
  3. อย่าเก็บข้อมูล sensitive โดยไม่ encrypt — localStorage ทุกคนเปิดเห็นได้ผ่าน DevTools

ตอนใช้ axios

  1. เลือก instance ที่เหมาะสม (AxiosBO, axiosAPI, axios-i3)
  2. ถ้าต้องการ refresh token อัตโนมัติ ใช้ AxiosBO() — มี interceptor ครบ
  3. ถ้า endpoint ต้องการ apiKey แต่ไม่ bearer token ใช้ factory แบบ AxiosBO(false) เพื่อข้าม apiKey header

ตอนจะ override AntD component

  1. หา className ของ component ใน DevTools (มักขึ้นต้นด้วย .ant-)
  2. เพิ่ม className ของตัวเองใน JSX (<Button className="my-btn">)
  3. เขียน CSS rule ใน main.css หรือ styles.css target .my-btn หรือ .my-btn .ant-btn
  4. ห้ามแก้ custom-antd.css

ตอนจะ sync state ระหว่าง 2 หน้า

  1. อย่าพยายาม sync ผ่าน global variable
  2. query ข้อมูลใหม่จาก server ในหน้าปลายทาง
  3. ถ้าจำเป็นจริง ๆ เก็บใน localStorage แล้วอ่านในหน้าปลายทาง (แต่ต้องจัดการ staleness)

ตารางสรุป dependency ที่เกี่ยวข้อง

dependencyเวอร์ชันบทบาทใน state/style/locale
react^16.14.0base สำหรับ class component + this.state
antd^3.26.20UI library (stuck บน v3 เพราะ form-decorator API)
moment^2.29.4date library (locale th + พ.ศ.)
moment-precise-range-plugin^1.3.0คำนวณช่วงเวลาแม่นยำ (อายุงาน)
styled-components^5.3.5scoped CSS (ใช้น้อย)
axios^0.21.4REST instance หลายตัว
react-markdown^8.0.3render markdown (ใช้ใน Slip, News)
draftjs-to-html^0.9.1markdown → HTML สำหรับ dangerouslySetInnerHTML
thaidatepicker-react^0.2.2Thai date picker (ใช้น้อย ส่วนใหญ่ใช้ AntD)
workbox-webpack-plugin4.3.1PWA SW (สร้างไว้ แต่ unregister)

ข้อจำกัดของสถาปัตยกรรมนี้

ข้อจำกัดผลกระทบทางออกถ้าจะแก้
ไม่มี global statesync ระหว่างหน้ายากเพิ่ม React Context ใน Root
AntD v3 stuckไม่ได้ใช้ v4 feature (Tabs lazy, Form hooks)migrate 100+ page ไป v4 Form hooks
moment.js heavybundle ใหญ่ (~280 KB)ย้ายไป dayjs หรือ date-fns
custom-antd.css compiledoverride ยากrebuild จาก less source
console ถูกปิดdebug ฝั่ง prod ยากเปลี่ยนเป็น log level
dangerouslySetInnerHTML หลายที่XSS risksanitize ด้วย DOMPurify

ขั้นตอนถัดไป

ไป 4.4 Routing + auth gate