Skip to content

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

บทนี้อธิบายว่า vtrc-rc-backoffice จัดการ state อย่างไร (3 ระบบผสม), CSS กี่ชั้น, และจัดการ locale พ.ศ. อย่างไร


State — 3 ระบบผสม

vtrc-rc-backoffice มีระบบ state 3 แบบที่ใช้พร้อมกัน:

┌─────────────────────────────────────────────────┐
│  1. Local component state (class)               │
│     ├── this.state / this.setState              │
│     └── module เก่า (Leave, ManageUser, ...)    │
├─────────────────────────────────────────────────┤
│  2. Local component state (hook)                │
│     ├── useState                                │
│     └── module ใหม่ที่ไม่ได้ share state         │
├─────────────────────────────────────────────────┤
│  3. Global state (Recoil)                       │
│     ├── useRecoilState / useRecoilValue         │
│     └── module WorkForce, PMS                   │
├─────────────────────────────────────────────────┤
│  4. Persisted state (localStorage)              │
│     ├── token, refreshToken, apiKey             │
│     └── work-force.service.js helpers           │
└─────────────────────────────────────────────────┘

1. Local component state (class)

js
class MyComponent extends React.Component {
  state = {
    users: [],
    loading: false,
    modalVisible: false,
  };

  // อัปเดต
  this.setState({ loading: true });

  // อัปเดตโดยใช้ค่าเดิม
  this.setState((prev) => ({ count: prev.count + 1 }));
}

ข้อดี

  • เรียบง่าย, แยกจาก component อื่นโดยสมบูรณ์
  • ไม่ต้อง setup library

ข้อเสีย

  • ไม่ share ข้าม component — ถ้า 2 component ต้องการ state เดียวกัน ต้องยกขึ้น parent (prop drilling) หรือใช้ Recoil
  • refresh browser แล้ว state หาย

เมื่อไรใช้

  • state ที่ใช้เฉพาะใน component นั้น (เช่น modalVisible, loading)
  • module เก่าที่ไม่ได้ใช้ Recoil

2. Local component state (hook — useState)

js
const MyComponent = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [modalVisible, setModalVisible] = useState(false);

  // อัปเดต
  setLoading(true);
  setUsers([...users, newUser]);
};

ใช้ใน module ใหม่ที่ไม่ต้อง share state

เช่น ในหน้า form เล็ก ๆ ที่ใช้แค่ใน component เดียว


3. Global state (Recoil)

js
// นิยาม atom
export const employeeListState = atom({
  key: "employeeListState",
  default: [],
});

// ใช้ใน component
const [employees, setEmployees] = useRecoilState(employeeListState);
const employees = useRecoilValue(employeeListState);  // อ่านอย่างเดียว
const setEmployees = useSetRecoilState(employeeListState);  // เขียนอย่างเดียว

ดูรายละเอียดใน บท 7.6

โครงสร้าง atom

src/recoil/states/
├── WorkForce/
│   ├── Employee/
│   │   ├── index.js          ← atom: employeeListState, employeeLoadingState
│   │   ├── currentId.js      ← atom: currentEmployeeIdState
│   │   ├── filters.js        ← atom: employeeFiltersState
│   │   ├── modal.js          ← atom: employeeModalState
│   │   └── viewMode.js       ← atom: employeeViewModeState
│   ├── PositionManage/
│   └── ...
├── PMS/
└── Recruitment/

Pattern ที่พบใน WorkForce module

แต่ละ entity (Employee, Position, Manpower) มี atom แยกตามหน้าที่:

Atomหน้าที่
<Entity>ListStateรายการในตาราง
<Entity>LoadingStateสถานะ loading
<Entity>FiltersStateตัวกรอง (search, departmentId, page)
<Entity>ModalStateสถานะ modal (visible, editingId)
<Entity>ViewModeStateโหมดการดู (view, edit)

ข้อดี

  • share ข้าม component โดยไม่ต้อง prop drilling
  • debug ง่าย — ดูได้ใน React DevTools
  • ทำงานกับ React 16 เป็นธรรมชาติ

ข้อเสีย

  • state ไม่ persist — refresh แล้วหาย
  • เพิ่ม dependency — Recoil 0.7

4. Persisted state (localStorage)

vtrc-rc-backoffice ใช้ localStorage สำหรับ:

Keyที่เก็บที่ใช้อ่าน
tokenLogin, PathLogingetToken() / isLoggedIn() (auth.js)
refreshTokenLogin, PathLogingetToken() / refreshTokenBO
isRememberLoginสลับ localStorage vs sessionStorage
currentEmployeeId (WorkForce)คลิก row ในตารางwork-force.service.js
viewOnly (WorkForce)toggle modework-force.service.js
searchList (WorkForce)cache ผลค้นหาwork-force.service.js

Helper — work-force.service.js

js
// src/utils/work-force.service.js (concept)
export const setCurrentEmployeeId = (id) => {
  localStorage.setItem("currentEmployeeId", id);
};

export const getCurrentEmployeeId = () => {
  return localStorage.getItem("currentEmployeeId");
};

export const setViewOnly = (flag) => {
  localStorage.setItem("viewOnly", flag ? "true" : "false");
};

export const getViewOnly = () => {
  return localStorage.getItem("viewOnly") === "true";
};

Helper — storage.js (with expiry)

js
// src/utils/storage.js
export const setWithExpiry = (key, value, ttl) => {
  const now = new Date();
  const item = {
    value: value,
    expiry: now.getTime() + ttl,
  };
  localStorage.setItem(key, JSON.stringify(item));
};

export const getWithExpiry = (key) => {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null;

  const item = JSON.parse(itemStr);
  const now = new Date();

  if (now.getTime() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
};

ข้อระวังเรื่อง localStorage

  1. XSS สามารถขโมยได้ — JavaScript ใด ๆ ที่รันในแอปสามารถอ่าน localStorage ได้
  2. ไม่ sync ข้าม tab — ถ้าผู้ใช้เปิด 2 tab อาจได้ state ต่างกัน
  3. refresh แล้วค้าง — ถ้าโค้ดลืมเคลียร์ (เช่น token หมดอายุแต่ยังอยู่ใน localStorage)

Styling — CSS หลายชั้น

vtrc-rc-backoffice มี CSS จากหลายแหล่ง:

┌────────────────────────────────────────────────┐
│  1. AntD v4 default (antd.css)                 │
│     import ใน src/index.js                     │
├────────────────────────────────────────────────┤
│  2. Custom AntD override (custom-antd.css)     │
│     21,189 บรรทัด — compiled จาก sass         │
├────────────────────────────────────────────────┤
│  3. main.css (4,644 บรรทัด)                    │
│     global styles + utilities                  │
├────────────────────────────────────────────────┤
│  4. theme/*.css (หลายไฟล์)                     │
│     themeplate.css (typo), template.css, ...   │
├────────────────────────────────────────────────┤
│  5. work-force.css (794 บรรทัด)                │
│     เฉพาะ WorkForce module                     │
├────────────────────────────────────────────────┤
│  6. Component-scoped CSS (.css คู่กับ .js)      │
│     เช่น MyComponent/index.css                  │
├────────────────────────────────────────────────┤
│  7. Inline style (style={{...}})               │
│     ใช้บ่อยใน component                         │
└────────────────────────────────────────────────┘

1. AntD v4 default

js
// src/index.js
import "antd/dist/antd.css";

เป็น CSS ของ AntD ที่ import ตรง ๆ — ใหญ่ประมาณ 600 KB


2. Custom AntD override — custom-antd.css

ไฟล์ใหญ่ที่สุดในแอป (21,189 บรรทัด) — เป็น AntD theme override:

css
/* src/config/custom-antd.css (ตัวอย่าง concept) */
.ant-btn {
  border-radius: 6px;
  font-family: "Kanit", sans-serif;
}

.ant-table-thead > tr > th {
  background: #f0f2f5;
  font-weight: 600;
}

.ant-modal-content {
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

ที่มา

ไฟล์นี้เป็น compiled output จาก sass — ทีมใช้ Prepros (desktop app) คอมไพล์ assets/sass/main.scssassets/css/main.css + เพิ่ม override ใน custom-antd.css

ข้อระวัง

  1. ห้ามแก้ custom-antd.css โดยตรง — เพราะเป็น compiled output การแก้จะถูกเขียนทับตอน compile ใหม่
  2. แก้ที่ sass source แทนassets/sass/main.scss + assets/sass/work-force.scss
  3. Prepros ไม่ได้ลงใน CI — ทำให้การ build ใน Docker อาจไม่ได้คอมไพล์ sass ใหม่

3. main.css (4,644 บรรทัด)

js
// src/index.js
import "./assets/css/main.css";

ประกอบด้วย:

หมวดตัวอย่าง
Reset / normalize*, body, html
Typography.title, .subtitle, .text-link
Layout utilities.flex, .flex-col, .mb-2, .mt-4
Color tokens.text-primary, .bg-secondary
Component helpers.card, .form-section, .table-action

ข้อระวัง

  • ใช้ร่วมกับ AntD class — บางครั้ง conflict (เช่น .ant-btn override)
  • มี utility คล้าย Tailwind แต่ไม่ใช่ Tailwind

4. theme/*.css — หลายไฟล์ใกล้เคียงกัน

src/assets/css/theme/
├── template.css        ← ชื่อที่ถูก
├── themeplate.css      ← typo (เพิ่ม h)
├── theplate.css        ← typo (หาย m)
├── time_attendace.css  ← typo (หาย d — ควรเป็น attendance)
└── ...

ปัญหา: มี 3 ไฟล์ที่ชื่อใกล้เคียงกัน (template/themeplate/themplate) — ไม่ชัดว่าตัวจริงตัวไหน

ผล: มีการ import ผิดไฟล์ได้ง่าย ทำให้ style ไม่ทำงาน

คำแนะนำ

  • grep หาการ import แต่ละไฟล์เพื่อดูว่าตัวจริงคือตัวไหน
  • อย่าสร้างไฟล์ใหม่ที่ชื่อใกล้เคียง

5. work-force.css (794 บรรทัด)

เฉพาะ WorkForce module — import ใน WorkForce component:

js
// pages/WorkForce/Employee/index.js (concept)
import "assets/css/work-force.css";

แยกจาก main.css เพราะ WorkForce มี design เฉพาะ (grid layout, custom card)


6. Component-scoped CSS

แต่ละ component มีไฟล์ .css คู่กับ .js:

src/components/SiderLayout/
├── index.js
├── index.css        ← style เฉพาะ SiderLayout
└── index_bk.js      ← snapshot (ห้ามใช้)

ข้อดี

  • ไม่ leak style ไปยัง component อื่น

ข้อเสีย

  • ไม่ใช่ CSS Modules — ไม่มี scope จริง, class name อาจชนกัน
  • import ทุกครั้งที่ component ถูก import — ทำให้ CSS โหลดทั้งหมด

7. Inline style

jsx
<div style={{ display: "flex", gap: 8 }}>
  <Button style={{ marginRight: 8 }}>Cancel</Button>
  <Button type="primary">Save</Button>
</div>

ใช้บ่อย — โดยเฉพาะใน module ใหม่

ข้อระวัง

  • ไม่สามารถ override ด้วย media query — ทำ responsive ยาก
  • ไม่ reuse ได้ — ทำให้โค้ดซ้ำ

CSS — สรุปความซับซ้อน

ชั้นขนาดเสี่ยง conflict
AntD default600 KBต่ำ (well-defined)
custom-antd.css21,189 บรรทัดปานกลาง (override)
main.css4,644 บรรทัดสูง (utility + class)
theme/*.css~1,000 บรรทัดสูง (typos, ซ้ำ)
work-force.css794 บรรทัดต่ำ (isolated)
Component CSS~5,000 บรรทัดรวมปานกลาง
Inline(ไม่นับ)ปานกลาง

รวม — CSS ที่โหลดตอนเปิดแอป ~35,000 บรรทัด


Locale — พ.ศ. (Buddhist Era)

vtrc-rc-backoffice ใช้ moment.js สำหรับจัดการวันที่ และแปลง ค.ศ. → พ.ศ. ด้วย manual +543

Pattern

js
// src/utils/formatdate.js (concept)
import moment from "moment";
import "moment/locale/th";
moment.locale("th");

export const formatDate = (date, format = "DD/MM/YYYY") => {
  if (!date) return "";
  return moment(date).format(format);
};

export const formatDateBE = (date, format = "DD MMMM YYYY") => {
  if (!date) return "";
  // moment('th') locale ปรับปีเป็นพ.ศ.อัตโนมัติ — แต่ทีมยังใช้ manual +543 บางจุด
  const m = moment(date);
  const beYear = m.year() + 543;
  return m.year(beYear).format(format);
};

export const parseBE = (dateStr) => {
  // รับ "10/07/2569" → แปลงเป็น Date object (ค.ศ.)
  const [day, month, yearBE] = dateStr.split("/");
  const yearAD = parseInt(yearBE) - 543;
  return new Date(yearAD, parseInt(month) - 1, parseInt(day));
};

การใช้ใน component

jsx
// แสดงวันที่ในตาราง
const columns = [
  {
    title: "วันที่สร้าง",
    dataIndex: "createdAt",
    render: (date) => formatDateBE(date, "DD/MM/YYYY"),
  },
];

// รับจาก AntD DatePicker
<Form.Item name="startDate" label="วันที่เริ่ม">
  <DatePicker
    format="DD/MM/YYYY"
    onChange={(date, dateStr) => {
      // dateStr = "10/07/2569"
      const isoDate = parseBE(dateStr);
      form.setFieldsValue({ startDate: isoDate });
    }}
  />
</Form.Item>

ข้อระวังเรื่องพ.ศ.

  1. moment('th') ปรับปีอัตโนมัติ — แต่ทีมยังใช้ manual +543 ในบางจุด ทำให้สับสน
  2. backend ส่ง/รับ ค.ศ. — frontend แปลงพ.ศ. ตอนแสดงผลเท่านั้น
  3. moment ล้าสมัยแล้ว — ควรใช้ dayjs หรือ date-fns แทน (ขนาดเล็กกว่า)
  4. ไม่มี i18n สำหรับภาษาอื่น — ทุกข้อความฝังเป็นภาษาไทยในโค้ด

Pattern ที่ต้องระวัง

js
// ❌ ผิด — บวก 543 แล้วส่งไป server (server จะเก็บปี 2666)
const wrongDate = moment().year(moment().year() + 543).toISOString();
await saveDate({ date: wrongDate });

// ✓ ถูก — ส่ง ค.ศ. ไป server, แปลง พ.ศ. ตอนแสดงผลเท่านั้น
await saveDate({ date: moment().toISOString() });
// ตอนแสดงผล
formatDateBE(savedDate);

Locale — moment ภาษาไทย

js
// ตั้งใน index.js หรือ formatdate.js
import "moment/locale/th";
moment.locale("th");

// ใช้
moment().format("MMMM"); // → "กรกฎาคม" (แทน "July")

ข้อระวัง

  • moment.locale("th") ทำให้ format เดือนเป็นภาษาไทย แต่ year ก็ถูกปรับเป็นพ.ศ.ด้วย (ถ้าใช้ YYYY format)
  • ถ้าต้องการ ค.ศ. — ใช้ moment.locale("en") ชั่วคราว

เปรียบเทียบกับ vtrc-web (ดู Volume 8 บท 6.8)

ด้านvtrc-rc-backofficevtrc-web
Stateclass + Recoilclass
Recoilใช้ไม่ใช้
CSS systems7 ชั้น5 ชั้น
custom-antd.css21,189 บรรทัด~10,000
theme typosมี (3 ไฟล์ใกล้กัน)ไม่มี
momentใช้ใช้
พ.ศ.manual +543 + localemanual +543

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

ตอนเพิ่ม state ใหม่

  1. ถ้าใช้เฉพาะใน component → useState หรือ this.state
  2. ถ้าต้อง share ข้าม component → สร้าง atom ใน Recoil
  3. ถ้าต้อง persist → localStorage + helper (storage.js หรือ work-force.service.js)

ตอนเพิ่ม CSS ใหม่

  1. อย่าแก้ custom-antd.css โดยตรง — แก้ที่ sass source
  2. ใช้ component-scoped CSS ถ้าเป็น style เฉพาะ component
  3. ใช้ inline เฉพาะกรณีจำกัด (1-2 properties)

ตอนจัดการวันที่

  1. เก็บเป็น ค.ศ. ใน state / server
  2. แปลง พ.ศ. ตอนแสดงผลด้วย formatDateBE()
  3. รับจาก user → แปลง พ.ศ. → ค.ศ. ด้วย parseBE() ก่อนส่ง server

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

ไป บท 7.9 Security + performance