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 กลาง |
| Styling | AntD v3 default + compiled override + SCSS + utility + styled-components | ผสม 5 ชั้น |
| Locale | AntD LocaleProvider th_TH + moment.js th locale | ภาษาไทย + พ.ศ. |
| Date library | moment.js 2.29 (locale th) | heavy, ไม่ได้ย้ายไป dayjs/date-fns |
| Theme | hardcoded colors ใน CSS | ไม่มี design token, ไม่มี theme switcher |
| Fonts | Kanit (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 ใน component | state ของหน้า (loading, data, form values, modal flag) | component |
localStorage | token, refreshToken, profile, noti counter, remember flag, encrypted credentials | ข้าม tab + ข้ามวัน |
sessionStorage | token (ถ้าไม่ remember), tab-specific flag | tab ปัจจุบัน |
นอกจากนี้ยังมี 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') อ่านตรง ๆ ในหลาย page | sync ด้วยมือ |
| สลับหน้าแล้ว form state หาย | ไม่เก็บ — ถามว่า "ออกจากหน้าไหม" ก่อน history.push | UX ไม่ราบรื่น |
ถ้าวันหน้าทีมอยากใส่ global state ตัวเลือกที่เข้ากับ pattern เดิมที่สุดคือ React Context API (built-in, ไม่ต้อง dependency) — แต่ต้องครอบใน Root component ที่ src/index.js:80
localStorage / sessionStorage — key ที่ใช้บ่อย
| key | ที่เก็บ | ตัวอย่างค่า |
|---|---|---|
token | JWT ปัจจุบัน | eyJhbGc... |
refreshToken | refresh token | eyJhbGc... |
isRemember | flag remember me | 'true' |
appUsername | username ที่ login | 'sso-12345' |
sessionID | session id | UUID |
lsid | login session id | UUID |
organization | org id | '3F3BF3AD-...' |
IsTrial | flag ทดลองใช้ | 'true' |
objectValuePF | profile object cache | JSON string |
objectValueEM | employee object cache | JSON string |
profileDetailPW | encrypted password | AES encrypted |
profileDetailUN | encrypted username | AES encrypted |
expiredDate | วันที่ password จะหมดอายุ | ISO date |
localtoken | client id (random) | UUID |
reCaptchaCheck | flag 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:99 | logout — ล้างทั้งสอง 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
- ห้ามอ่าน
localStorage.getItem('token')ตรง ๆ — ใช้getToken()เสมอ เพราะต้องเลือก localStorage หรือ sessionStorage ตาม flagisRemember - ห้ามเก็บ object ใน storage โดยไม่
JSON.stringify—localStorageเก็บ string เท่านั้น - ห้าม sync state ระหว่าง component ผ่าน global variable (เช่น
window.foo = ...) — ใช้ props หรือ query ใหม่ - ถ้า state ใหญ่เกินไปใน component เดียว ให้แบ่งเป็น sub-component แล้วส่ง props ลงไป อย่าใช้ state library มาช่วย
ตัวอย่าง pattern ที่ใช้จริง
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 ทับ
/* บรรทัด 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 บรรทัด)
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.css | output จาก sass/main.scss — global style หลัก |
main_bk.css | backup ของ main.css (อย่าแก้) |
app.css | utility เล็ก ๆ สำหรับ layout บางส่วน |
variables.css | CSS 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:
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)
จากซ้ำไปหนา:
- AntD default —
.ant-btn { ... }จากantd.css custom-antd.css— compiled override 21,190 บรรทัดmain.css— global SCSS ที่ compile แล้วstyles.css— utility class- 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>:
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 ภาษาไทย:
import { LocaleProvider } from 'antd';
import th_TH from 'antd/lib/locale/provider/th_TH';และครอบ <App /> ใน Root component (vtrc-web/src/index.js:80-90):
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 ไทย:
import moment from 'moment';
import 'moment/locale/th';
moment.locale('th');moment.js locale th ทำ 2 อย่างอัตโนมัติ:
- แปลงชื่อเดือนเป็นไทย —
moment().format('MMMM')ได้กรกฎาคมแทนJuly - เปลี่ยน year offset —
moment().format('YYYY')ยังได้ C.E. แต่moment().format('LL')ได้10 กรกฎาคม 2569(พ.ศ.)
moment.updateLocale override
vtrc-web/src/index.js:36-45 มีการปรับแต่ง locale ไทยเพิ่มเติม:
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 ไว้ทั้งหมด:
// เก็บฟังก์ชัน 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:
export const changeYear = (year) => {
return year + 543;
};และ helper ที่จัดรูปแบบวันที่ภาษาไทย + ปี พ.ศ.:
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:30 | detail ต่าง ๆ |
changeFormatMonthYearThai(date) | ตุลาคม 2569 | report |
changeFormatdateThai(date) | เหมือน changeFormatdate | leave |
changeOnlyMonthSlip(date) | กรกฎาคม (เดือนอย่างเดียว) | Slip dropdown |
changeYear(year) | 2569 (เลขปีอย่างเดียว) | คำนวณ |
pushAge(birthDate) | อายุเป็นปี | profile |
pushAgeWork(startDate) | อายุงานเป็นปี | profile |
pushAgeDate(birthDate, deathDate) | อายุโดยระบุวันสิ้นสุด | กรณีเสียชีวิต |
changeFormatdateStartEnd(start, end) | 10 ก.ค. 2569 - 15 ก.ค. 2569 | leave list |
dateFormatNumber(values) | 25691010 | ใช้ใน query |
disabledDate(current) | disable วันที่ผ่านไปแล้ว | DatePicker |
กฎเหล็กเรื่อง พ.ศ.
- ห้าม +543 ตรง ๆ ใน component — ใช้ helper
changeFormatdate()หรือchangeYear()เสมอ - ห้ามใช้
moment().year()ตรง ๆ เพื่อแสดงผล — จะได้ C.E. - ถ้าจะแสดงปีเป็น พ.ศ. ใน dropdown — ใช้
changeYear(year)ก่อน set เป็น options - moment.js locale
thจะ format ให้เป็น พ.ศ. อัตโนมัติเมื่อใช้.format('LL')หรือ.format('LLLL')— แต่.format('YYYY')ยังได้ C.E. - ถ้าข้อมูลจาก server ส่ง พ.ศ. มาแล้ว ห้าม +543 ซ้ำ — ตรวจก่อนเสมอ
ความซ้ำซ้อนใน helper
helper หลายตัว duplicate logic (mapping เดือน 12 ตัวเขียนซ้ำในแต่ละ function) — ถ้าจะ refactor แนะนำให้สร้าง constant MONTH_TH กลาง แล้วให้ทุก helper import ใช้
ปัจจุบัน monthThai array ถูกประกาศซ้ำใน 7+ function ใน formatdate.js:
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 color | rgba(0, 0, 0, 0.65) (main.scss) |
| font size | 16px (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.4 — vtrc-web/package.json:29)
1. axios-bo.js — instance หลักพร้อม interceptor
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 ได้ยาก:
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.1 — vtrc-web/package.json:109) แต่ฝั่งแอปไม่ register จึงไม่ทำงาน
ถ้าจะเปิด PWA จริง ต้องเปลี่ยนเป็น serviceWorker.register() และออกแบบ cache invalidation
dangerouslySetInnerHTML — ใช้หลายที่
grep พบการใช้ dangerouslySetInnerHTML ใน 20+ ไฟล์ — ส่วนใหญ่เพื่อ render markdown content หรือ HTML ที่ได้จาก server
| ไฟล์ | ใช้ทำอะไร |
|---|---|
Slip/index.js | render text payroll slip (HTML) |
Notification/index.js | render ข้อความ notification |
News/index.js, DetailNews/index.js | render เนื้อหาข่าว |
Policy/index.js, PolicyDetail/index.js | render policy text |
PmsIndicators/form/... | render modal content |
Orientation/index.js | render 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.3 — vtrc-web/package.json:88) ในหน้า Slip
เคล็ดลับตอนทำงานจริง
ตอน debug "ปีที่แสดงผิด"
- เช็คว่าใช้ helper อะไร (
changeFormatdatevsmoment().format('YYYY')) - เช็คว่าข้อมูลจาก server เป็น C.E. หรือ พ.ศ. แล้ว — ถ้า server ส่ง พ.ศ. มาแล้ว ห้าม +543 ซ้ำ
- เช็ค locale ของ moment —
.locale()ต้องเป็น'th'
ตอน debug "style ไม่ทำงาน"
- ตรวจ className ใน DevTools → ดูว่ามี CSS rule ไหน match
- ตรวจ specificity — AntD override มักจะ
!importantบ่อย - ตรวจ load order ใน
index.js—main.cssโหลดก่อนcustom-antd.cssหรือไม่ - ตรวจ OS/browser class — บาง rule ทำงานเฉพาะ
.win-chromeหรือ.os-macos
ตอนจะเก็บข้อมูลใน localStorage
- ใช้ helper
setWithExpiryถ้าต้องการ expiry - JSON.stringify object ทุกครั้ง
- อย่าเก็บข้อมูล sensitive โดยไม่ encrypt — localStorage ทุกคนเปิดเห็นได้ผ่าน DevTools
ตอนใช้ axios
- เลือก instance ที่เหมาะสม (
AxiosBO,axiosAPI,axios-i3) - ถ้าต้องการ refresh token อัตโนมัติ ใช้
AxiosBO()— มี interceptor ครบ - ถ้า endpoint ต้องการ apiKey แต่ไม่ bearer token ใช้ factory แบบ
AxiosBO(false)เพื่อข้าม apiKey header
ตอนจะ override AntD component
- หา className ของ component ใน DevTools (มักขึ้นต้นด้วย
.ant-) - เพิ่ม className ของตัวเองใน JSX (
<Button className="my-btn">) - เขียน CSS rule ใน
main.cssหรือstyles.csstarget.my-btnหรือ.my-btn .ant-btn - ห้ามแก้
custom-antd.css
ตอนจะ sync state ระหว่าง 2 หน้า
- อย่าพยายาม sync ผ่าน global variable
- query ข้อมูลใหม่จาก server ในหน้าปลายทาง
- ถ้าจำเป็นจริง ๆ เก็บใน
localStorageแล้วอ่านในหน้าปลายทาง (แต่ต้องจัดการ staleness)
ตารางสรุป dependency ที่เกี่ยวข้อง
| dependency | เวอร์ชัน | บทบาทใน state/style/locale |
|---|---|---|
react | ^16.14.0 | base สำหรับ class component + this.state |
antd | ^3.26.20 | UI library (stuck บน v3 เพราะ form-decorator API) |
moment | ^2.29.4 | date library (locale th + พ.ศ.) |
moment-precise-range-plugin | ^1.3.0 | คำนวณช่วงเวลาแม่นยำ (อายุงาน) |
styled-components | ^5.3.5 | scoped CSS (ใช้น้อย) |
axios | ^0.21.4 | REST instance หลายตัว |
react-markdown | ^8.0.3 | render markdown (ใช้ใน Slip, News) |
draftjs-to-html | ^0.9.1 | markdown → HTML สำหรับ dangerouslySetInnerHTML |
thaidatepicker-react | ^0.2.2 | Thai date picker (ใช้น้อย ส่วนใหญ่ใช้ AntD) |
workbox-webpack-plugin | 4.3.1 | PWA SW (สร้างไว้ แต่ unregister) |
ข้อจำกัดของสถาปัตยกรรมนี้
| ข้อจำกัด | ผลกระทบ | ทางออกถ้าจะแก้ |
|---|---|---|
| ไม่มี global state | sync ระหว่างหน้ายาก | เพิ่ม React Context ใน Root |
| AntD v3 stuck | ไม่ได้ใช้ v4 feature (Tabs lazy, Form hooks) | migrate 100+ page ไป v4 Form hooks |
| moment.js heavy | bundle ใหญ่ (~280 KB) | ย้ายไป dayjs หรือ date-fns |
custom-antd.css compiled | override ยาก | rebuild จาก less source |
| console ถูกปิด | debug ฝั่ง prod ยาก | เปลี่ยนเป็น log level |
dangerouslySetInnerHTML หลายที่ | XSS risk | sanitize ด้วย DOMPurify |