Tailwind CSS เทคนิคและแนวปฏิบัติแบบ Best Practices
Table of contents
Intro
สวัสดีครับ บทความนี้จะพูดถึง Tailwind CSS สำหรับคนที่เคยใช้งานมาแล้ว เนื่องจากเทคโนโลยีที่ได้ใช้งานบ่อยใช้เยอะช่วงนี้ พอใช้เรียนรู้และใช้งานแบบ Basic มาได้สักพัก ก็ไปค้นหาข้อมูลที่คนอื่น ๆ เขาเขียนและนำนำไว้เพื่อดูเทคนิค (Tips
) และแนวปฏิบัติที่ดีที่สุด (Best Practices
) ทำให้เจอข้อมูลที่น่าสนใจหลายอย่าง
บทความน ี้จึงเป็นการรวบรวมและประเด็นที่น่าสนใจที่ค้นพบ และโน้ต ๆ ไว้อ่าน 🚀
Tailwind CSS
Tailwind CSS คือ Utility-First CSS Framework เป็น CSS Selector แบบสำเร็จที่เรามานำ Class เล็ก ๆ มาประกอบกันขึ้นอยู่กับวัตถุประสงค์การใช้งานสามารถนำไป Implement ได้หลายแบบ เช่น
- ใช้แค่ Tailwind เพียว ๆ อย่าง Tailwind UI
- Tailwind CSS + Ant-Design
- Tailwind CSS + Material UI
- Tailwind CSS + UI อื่น ๆ
Best Practices
1. ใช้ States แทนการใช้ Booleans
ระวังการใช้ Booleans
<Button primary disabled secondary active />
ถ้าสังเก ตปัญหาของวิธีข้างบน คือการจัดเรียงที่เป็นแบบ invalid มากเกินไปทำให้อ่านยาก
<Button state={Button.state.ACTIVE} variant={Button.variant.PRIMARY} />
ทำให้ชัดไปเลยไม่มีทางที่จะไม่เป็น primary และไม่มีทางที่จะไม่เป็น active
2. ClassName เป็น prop ที่อาจจะเป็น code smell
<Button variant={Button.variant.PRIMARY} className="p-4" />
เมื่อสร้าง Button แบบฉบับ Version ของตัวเอง แต่มันยากที่จะรู้ว่าอะไรอยู่ข้างใน State บ้าง (อาจจะมี className อีกตัวในนั้น) ที่จะทำให้ ClassName ที่เป็น prop อาจจะเป็น code smell
3. ใช้ local state แทน global state
เพื่อให้ชัดว่า State มาจากไหน
4. ใช้ props แทน state
มันจะดีกว่าเรื่องประสิทธิภาพถ้าใช้ props แทนการ sync ค่าจาก state
5. เป็นไปได้ไม่ต้องใช้ props เลยจะดีมาก
ใช้ Props เท่าที่จำเป็น หรือไม่ต้องใช้เลย
6. ใช้ components แทน props
<Button big fullWidth/>
// ควรเป็น
<BigButton/>
// หรือ
<FullWidthButton/>
7. ใช้ children แทน props
<Button icon={''}>Apple</Button>
// ควรเป็น
<Button> Apple</Button>
import React, { ReactNode } from 'react';
import { classNames } from '../util/classNames';
enum Variant {
RED,
YELLOW,
GREEN,
BLUE,
}
enum Size {
LARGE,
SMALL,
}
type Props = {
variant: Variant;
children?: ReactNode;
size: Size;
};
const SIZE_MAPS: Record<Size, string> = {
[Size.SMALL]: 'px-2.5 text-xs',
[Size.LARGE]: 'px-3 text-sm',
};
const VARIANT_MAPS: Record<Variant, string> = {
[Variant.RED]: 'bg-red-100 text-red-800',
[Variant.YELLOW]: 'bg-yellow-100 text-yellow-800',
[Variant.GREEN]: 'bg-green-100 text-green-800',
[Variant.BLUE]: 'bg-blue-100 text-blue-800',
};
export function Badge(props: Props) {
const { children, variant, size } = props;
return (
<span
className={classNames(
'inline-flex items-center py-0.5 rounded-full font-medium leading-4 whitespace-no-wrap',
VARIANT_MAPS[variant],
SIZE_MAPS[size],
)}
>
{children}
</span>
);
}
Badge.defaultProps = {
variant: Variant.GRAY,
size: Size.SMALL,
};
Badge.variant = Variant;
Badge.size = Size;
<Badge variant={Badge.variant.RED} size={Badge.size.BIG}>Text</Badge>
เอาจริง ๆ ประเด็นนี้ทำวิธีแบบนี้ก็ดี ✅ และถ้าจะไม่ใช้ก็ไม่ได้ซีเรียส 😌 เพราะว่าถ้าเขียนเป็น Typescript ใช้กับ IDE ที่ฉลาด ๆ หน่อยเราจะรู้ได้หมดกว่า Component นั้น ๆ กำหนด props ไว้แบบไหนบ้าง
Types of Tailwind components
Non-components (snippets)
บางครั้งสิ่งที่คุณพยายามทำเป็นรูปแบบซ้ำ ๆ (Components) แต่แทนที่จะเป็น Component หากรายละเอียดจะไม่ซ้ำกันสำหรับ Component ส่วนใหญ่ และไม่จำเป็นต้องเชื่อมโยงกับ Component อื่นๆ มันก็คือ Non-components จะเรียกว่า Template
หรือ Snippet
ตัวอย่างที่ดีของเรื่องนี้คืออะไร ? อย่าง Tailwind ก็มีแบบจ่ายเงินและแบบฟรีสำหรับใช้งานคือ Tailwind UI พวกนี้ก็เป็น Snippets ที่สามารถคัดแล้ววางใส่ใน App เพื่อสร้าง Prototyping แบบเร็ว ๆ พวกมันอาจกลายเป็นส่วนประกอบที่ใช้ซ้ำได้ในโค้ดของคุณในบางจุด
Small components
ตัวอย่างปุ่มใช้ 17 Utility classes และบางทีคุณอาจจะมี 12 ปุ่มที่มีการแชร์ 15 utility classes จาก 17 classes
<button type="button" class="inline-flex items-center px-2.5 py-1.5 border
border-transparent text-xs font-medium rounded shadow-sm text-white
bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2
focus:ring-offset-2 focus:ring-indigo-500">
Button text
</button>
ใน tailwind เคสแบบนี้คุณอาจจะใช้ @apply
directive เพื่อรวม common classes เพื่อสร้างเป็น class ปุ่มแบบใหม่ แต่เอาจริง ๆ วิธีนี้ส่วนตัวกลับไม่ค่อยชอบ เพราะว่าวิธีชอบวิธีการสร้าง components ประกอบกันและใช้วิธี class compose เวลาดูแลรักษามันอ่านง่ายกว่า
Small component (code smell)
ในบางเคสคุณอาจจะต้องการสร้าง Component สำหรับห่อหุ้มบางสิ่งจาก Styles อื่น เช่น การคลิก Event เพื่อ Handler นี่คือข้อยกเว้นสำเรื่อง classNames แบบ props ที่่จะเป็น code smell
และ เป็นการดีที่จะใช้ classNames เป็น prop และใส่ Utility classes บน component ที่ต้องการ
Large component (complex)
ใน components ขนาดใหญ่อาจจะประกอบไปด้วย Elements หลายอัน เพื่อกำหนดส่วนประกอบที่เป็นได้มากที่สุด และการใส่ค่าต่าง ๆ ลงใน props แต่มันก็ยากมากในการอ่านโค้ดเหล่านั้นและเข้าใจ State ของแต่ละ Version
จากการพยายามแทนที่ด้วย components over props
(6) และ children over components
(7) แต่เมื่อ App เติบโตขึ้นและเปลี่ยนแปลง แต่ละกรณีมีแนวโน้มที่จะแตกต่างกัน ทำให้คุณต้องเพิ่ม prop อื่นเพื่อรองรับและทำงานได้กับ Features ใหม่ ๆ
Large component (complex) เมื่อพวกมันมักจะเชื่อมโยงกัน จนกว่าคุณจะพบว่าคุณมี Component ปีศาจ ☠️ เมื่อถึงจุด ๆ นึงจะเข้าใจว่า นี่คือบทเรียนสำคัญ: อย่ากลัวที่จะทำลายส่วนประกอบขนาดใหญ่เหล่านั้นเมื่อมันเทอะทะ ❌
Utility classes
- ใช้ Utility classes น้อย ๆ เท่าที่เป็นไปได้ ตัวอย่าง
mx-2
แทนml-2 mr-2
และอย่ากังวลที่จะใช้p-4 lg:pt-8
แทนpt-4 lg:pt-8 pr-4 pb-4 pl-4
ที่ยาวและซับซ้อน - พิจารณาการใช้
prefix
ตัวอย่างblock lg:flex lg:flex-col lg:justify-center
แทนblock lg:flex flex-col justify-center
เพราะจะเคลียร์กว่า โดยflexbox
จะใช้กับlg:
ที่เป็นbreakpoint
และ ขนาดใหญ่larger
- สิ่งนึงที่ควรจำไว้คือ โดยปกติ
responsive utilities
ทั้งหมดจะกำหนดmin-width
นั่นคือsm:
แต่กลายเป็นว่ามันไม่ได้ apply แค่กับsmall viewports
เท่านั้นเพราะจะ apply กับsmall breakpoints
และlarge
ซึ่งสำหรับตัวอย่างlock sm:block md:flex lg:flex xl:flex
คุณสามารถใช้แค่md:flex
ซึ่งมันก็ให้ผลลัพธ์เหมือนกัน - อีกสิ่งนึงที่ควรรู้คือ
sm: breakpoint
โดยปกติจะไม่ได้เริ่มจากmin-width: 0
แบบที่คุณคิด แต่มันเริ่มที่min-width: 640px
ซึ่งถ้าคุณต้ องการปรับให้เล็กที่สุด (เล็กกว่า 640px) คุณควรตั้งค่าให้ apply ทุก breakpoints และจากนั้น override สำหรับทุก larger breakpoints ตัวอย่างblock sm:inline
ถ้าคุณต้องการ block สำหรับ smallest - เวลาจะทำอะไรเกี่ยวกับ margin ใช้
mt-*
และml-*
ควบคู่กันสำหรับ position ของ current element หรือใช้mb-*
และmr-*
สำหรับ position elements ที่ใกล้ชิดกัน - สามารถใช้
<div>
เป็น wrapper เพื่อทำpadding
แทนการทำmargins
ที่อาจจะช่วยหลีกเลี่ยงปัญหาที่อาจจะเกิดขึ้นกับmargin collapsing
เพราะไม่จำเป็นต้องพยายามทำ classname เดียวสำหรับแก้ปัญหานี้
Component classes
- อย่าพยายามใช้
@apply
หรือสร้าง class ใหม่ใด ๆ ถ้าไม่จำเป็นนอกเสียจากว่า utility classes ภายใน component มีการใช้ซ้ำร่วมกับ components อื่น ๆ จริงซึ่งในเคสแบบนี้การสร้าง component class ใหม่ด้วย@apply
อาจจะโอเค - อย่า
@apply
กับ component classes ใน components อื่น ตัวอย่างเช่นแยก.btn
และ.btn-blue
classes ออกจากันแทนการใช้แบบ.btn-blue { @apply btn; }
Dynamic classes
- เมื่อใช้ CSS classes หรือเป็นแบบ Generated dynamically อย่าใช้ string ที่เป็นแบบต่อกัน (concatenation) เพื่อสร้าง Full class แต่ให้ใช้วิธีการสลับระหว่าง Strings class ตัวอย่างใช้
{ maxPerRow == 2 ? 'sm:w-1/2' : 'sm:w-1/3' }
แทน{ 'sm:w-1/' + maxPerRow }
เพราะจะเป็นปัญหาสร้าง class ใหม่แทนการใช้ class ที่มีอยู่และส่งผลกับขนาดไฟล์ css เมื่อ build ด้วย PurgeCSS
Bonus
Function สำหรับใช้ combine เพื่อสร้าง Full string จาก utility classes
export function classNames(...classes: (false | null | undefined | string)[]) {
return classes.filter(Boolean).join(" ");
}
// ตัวอย่างการใช้งาน
<button className={classNames('this is always applied',
isTruthy && 'this only when the isTruthy is truthy',
active ? 'active classes' : 'inactive classes')}>Text</button>
References
- https://gist.github.com/sandren/0f22e116f01611beab2b1195ab731b63
- https://blog.logrocket.com/tailwind-css-tips-for-creating-reusable-react-components/
- https://theodorusclarence.com/blog/tailwindcss-best-practice
- https://gist.github.com/RobinMalfait/7cf7a0498039027a1d591f5f2b908a95
- https://gist.github.com/RobinMalfait/490a0560a7cfde985d435ad93f8094c5