SVG from Zero to Senior · Part 14 — SVG in React & JSX
Ship SVG the React way: inline vs SVGR, the JSX attribute gotchas (className, strokeWidth), building typed icon components with props and currentColor, data-driven SVG, and refs for measuring paths. With a live props playground.
Most of you ship SVG inside a component framework, and React is the most common. {Đa số các con ship SVG bên trong một framework component, và React phổ biến nhất.} The good news: everything you learned still applies — SVG is just JSX now. {Tin tốt: mọi thứ con học vẫn đúng — SVG giờ chỉ là JSX.} The catch: a handful of attribute and ergonomics gotchas trip up everyone exactly once. {Cái bẫy: một nắm khác biệt về thuộc tính và trải nghiệm làm ai cũng vấp đúng một lần.} Let’s get them out of the way. {Hãy dẹp chúng đi.}
Tune the props and watch both the rendered icon and the JSX you’d type stay in sync. {Chỉnh props và xem cả icon render lẫn JSX con sẽ gõ luôn đồng bộ.}
Open the full demo {Mở demo đầy đủ}: /tools/svg-react-demo/.
Note: this blog is pure Astro, so the demo above is vanilla JS simulating the React pattern. The code blocks below are real React/TSX you can paste into a project. {Lưu ý: blog này thuần Astro, nên demo trên là vanilla JS mô phỏng mẫu React. Các khối code dưới là React/TSX thật con dán vào dự án được.}
Three ways to get SVG into React {Ba cách đưa SVG vào React}
- Inline JSX — paste the SVG markup straight into a component. Full control, stylable, scriptable. {JSX inline — dán markup SVG thẳng vào component. Kiểm soát đủ, style và script được.}
- Import as a component (SVGR) —
import Logo from './logo.svg'then<Logo />. Most CRA/Vite/Next setups support this via SVGR. {Import thành component (SVGR) —import Logo from './logo.svg'rồi<Logo />.} - Import as a URL —
import url from './logo.svg'then<img src={url} />. Cached, but no styling into it. {Import thành URL —<img src={url} />. Cache được, nhưng không style vào trong.}
Senior heuristic: SVGR for static art, hand-written components for an icon system you control. {Quy tắc senior: SVGR cho art tĩnh, component viết tay cho hệ icon con kiểm soát.}
The JSX attribute gotchas {Các bẫy thuộc tính JSX}
When you paste raw SVG into JSX, React will complain. {Khi con dán SVG thô vào JSX, React sẽ phàn nàn.} The rules: {Các luật:}
class→className. {class→className.}- Hyphenated SVG attributes become camelCase:
stroke-width→strokeWidth,stroke-linecap→strokeLinecap,clip-path→clipPath,fill-opacity→fillOpacity. {Thuộc tính SVG có gạch nối thành camelCase.} - A few keep dashes as strings because they’re CSS-y or namespaced — but the modern ones (
xmlns,viewBox) you already write fine; React preservesviewBoxcasing. {Vài cái giữ gạch nối; nhưng các cái hiện đại con viết vẫn ổn.} - Dynamic values use braces:
width={size}, notwidth="size". {Giá trị động dùng ngoặc nhọn.} - Inline
styleis an object:style={{ fill: color }}. {styleinline là một object.}
// ❌ pasted HTML — React warns
<svg class="icon"><path stroke-width="2" /></svg>
// ✅ JSX
<svg className="icon"><path strokeWidth={2} /></svg>
A typed icon component {Một component icon có kiểu}
This is the pattern every design system uses. {Đây là mẫu mọi design system dùng.} Accept props, default color to currentColor, and you get a themeable, sizeable icon: {Nhận props, mặc định color là currentColor, và con có một icon đổi theme và đổi cỡ được:}
type IconProps = {
size?: number;
color?: string;
strokeWidth?: number;
className?: string;
};
export function StarIcon({
size = 24,
color = 'currentColor',
strokeWidth = 2,
className,
}: IconProps) {
return (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
className={className}
role="img"
aria-label="Favorite"
>
<path
d="M12 2l2.9 6.3 6.9.7-5.2 4.6 1.5 6.8L12 17.8 5.9 21.2l1.5-6.8L2.2 9.7l6.9-.7z"
fill={color}
/>
</svg>
);
}
<StarIcon size={32} className="text-lime-400" /> // color follows CSS via currentColor
<button><StarIcon /> Save</button> // inherits button's text color
currentColor is doing the heavy lifting again (Part 10): the icon’s fill follows the CSS color of whatever contains it, so one utility class re-themes it. {currentColor lại gánh việc nặng (Phần 10): fill của icon theo color CSS của thứ chứa nó, nên một class utility đổi theme.}
Data-driven SVG, the React way {SVG theo dữ liệu, kiểu React}
Everything from Part 9 becomes declarative — you .map() data into elements and let React reconcile the DOM: {Mọi thứ từ Phần 9 thành khai báo — con .map() dữ liệu thành element và để React điều phối DOM:}
function BarChart({ data }: { data: number[] }) {
const W = 320, H = 200, pad = 28;
const bw = (W - pad * 2) / data.length;
return (
<svg viewBox={`0 0 ${W} ${H}`}>
{data.map((v, i) => (
<rect
key={i}
x={pad + i * bw}
y={H - pad - (v / 100) * (H - pad * 2)}
width={bw * 0.6}
height={(v / 100) * (H - pad * 2)}
fill="currentColor"
/>
))}
</svg>
);
}
No manual createElementNS, no innerHTML — React handles node creation and updates. {Không createElementNS thủ công, không innerHTML — React lo việc tạo và cập nhật node.} This is why D3-in-React often hands DOM mutation to React and uses D3 only for the math (scales, layouts). {Đó là lý do D3-trong-React thường giao việc thay đổi DOM cho React và chỉ dùng D3 cho phần toán.}
Refs for measuring paths {Ref để đo path}
When you need getTotalLength() (line-drawing from Part 6) or getScreenCTM() (hit-testing from Part 9), grab the element with a ref in an effect: {Khi cần getTotalLength() (vẽ đường Phần 6) hay getScreenCTM() (hit-test Phần 9), lấy element bằng ref trong một effect:}
const pathRef = useRef<SVGPathElement>(null);
useEffect(() => {
const len = pathRef.current?.getTotalLength() ?? 0;
// set CSS variables / state for the dash animation
}, []);
return <path ref={pathRef} d="…" />;
Do DOM measurement in useEffect/useLayoutEffect, never during render. {Đo DOM trong useEffect/useLayoutEffect, không bao giờ trong lúc render.}
The master’s warnings {Lời cảnh báo của sư phụ}
- camelCase every hyphenated attribute or React drops it silently. {camelCase mọi thuộc tính có gạch nối nếu không React bỏ nó lặng lẽ.}
- Unique gradient/filter ids across instances. Two
<StarIcon>with the same internalid="glow"collide — generate ids withuseId(). {Id gradient/filter duy nhất giữa các instance. Hai icon cùngidnội bộ đụng nhau — sinh id bằnguseId().} keyon mapped elements, same as any React list. {keytrên element map, như mọi list React.}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Icon component {Component icon}: build a typed
<Icon>withsize/color/strokeWidthandcurrentColordefault. {dựng<Icon>có kiểu vớisize/color/strokeWidthvà mặc địnhcurrentColor.} - useId for a gradient {useId cho gradient}: render the same gradient-filled icon twice and confirm no id collision with
useId(). {render icon tô gradient hai lần và xác nhận không đụng id nhờuseId().} - Reactive sparkline {Sparkline phản ứng}: a component that re-draws a
<polyline>whenever itsdataprop changes. {một component vẽ lại<polyline>mỗi khi propdatađổi.}
What’s next {Phần tiếp theo}
You can now ship SVG cleanly in a component world. {Giờ con ship SVG gọn trong thế giới component.} For the grand finale, we build the most impressive interactive SVG of all. {Cho màn kết hoành tráng, ta dựng SVG tương tác ấn tượng nhất.} In Part 15 we make an interactive map with pan and zoom, driving the viewBox from Part 1 with the mouse wheel and drag — geographic-style paths, zoom-to-point math, and the smooth navigation behind every web map. {Ở Phần 15 ta làm một bản đồ tương tác có pan và zoom, lái viewBox từ Phần 1 bằng con lăn chuột và kéo — path kiểu địa lý, toán zoom-về-điểm, và sự điều hướng mượt sau mọi bản đồ web.}