jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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}

  1. 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.}
  2. 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 />.}
  3. Import as a URLimport 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:}

  • classclassName. {classclassName.}
  • Hyphenated SVG attributes become camelCase: stroke-widthstrokeWidth, stroke-linecapstrokeLinecap, clip-pathclipPath, fill-opacityfillOpacity. {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 preserves viewBox casing. {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}, not width="size". {Giá trị động dùng ngoặc nhọn.}
  • Inline style is an object: style={{ fill: color }}. {style inline 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 colorcurrentColor, 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 internal id="glow" collide — generate ids with useId(). {Id gradient/filter duy nhất giữa các instance. Hai icon cùng id nội bộ đụng nhau — sinh id bằng useId().}
  • key on mapped elements, same as any React list. {key trê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}

  1. Icon component {Component icon}: build a typed <Icon> with size/color/strokeWidth and currentColor default. {dựng <Icon> có kiểu với size/color/strokeWidth và mặc định currentColor.}
  2. 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().}
  3. Reactive sparkline {Sparkline phản ứng}: a component that re-draws a <polyline> whenever its data prop changes. {một component vẽ lại <polyline> mỗi khi prop data đổ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.}