---
title: "Add a JavaScript library"
audience: "anyone who needs a third-party JavaScript library"
description: "You'll add Chart.js with fsr add, draw a chart with it and see how FSR vendors the library and names it in the import map. Then you'll hit the…"
snippet_caption: "terminal"
snippet: "$ fsr add shop/app chart.js@4.4.7\nadded     chart.js                     chart.js/chart.bundle.mjs  202502 bytes"
---

import { Install } from "@src/ui/Install";

You'll add Chart.js with `fsr add`, draw a chart with it and see how FSR vendors the library and names it in the import map. Then you'll hit the missing-declarations failure and fix it.

<Install captured="0.x" />

## Add it

```shell-session
$ fsr add shop/app chart.js@4.4.7
added     chart.js                     chart.js/chart.bundle.mjs  202502 bytes
```

One file landed in `app/vendor/chart.js/`. The file is committed, so anyone who clones the repo already has the library.

`fsr add` also wrote the import map for you:

```json title="app/importmap.json" {4}
{
  "imports": {
    "react": "/static/js/vendor/react/react.bundle.mjs",
    "chart.js": "/static/js/vendor/chart.js/chart.bundle.mjs"
  }
}
```

That map is what the browser uses to resolve `import { Chart } from "chart.js"`. It is also a build-time allowlist: a bare import with no entry fails the build instead of 404ing in someone's browser.

## Add a subpath

Chart.js wants its auto-registering entry, which is a separate specifier. Ask for it by name:

```shell-session
$ fsr add shop/app chart.js@4.4.7/auto
added     chart.js/auto                chart.js/auto.bundle.mjs  196 bytes
```

Now `import { Chart } from "chart.js/auto"` resolves.

## Use it in a browser island

A chart needs a canvas and a layout pass, so this one belongs in the browser. That's the other island mode from [020](https://snapfirers.com/fsr/learn/020-build-a-counter).

```tsx title="app/src/ui/Sales.tsx"
import { useEffect, useRef } from "react";
import { Chart } from "chart.js/auto";

export function Sales({ labels, values }: { labels: string[]; values: number[] }) {
  const canvas = useRef<HTMLCanvasElement>(null);
  useEffect(() => {
    if (!canvas.current) return;
    const chart = new Chart(canvas.current, {
      type: "bar",
      data: { labels, datasets: [{ label: "Units", data: values }] },
    });
    return () => chart.destroy();
  }, [labels, values]);
  return <canvas ref={canvas} height={160} />;
}
```

Give it a page. The loader returns the figures:

```ts title="app/routes/sales/page.loader.ts"
import type { Ctx } from "@snapfire/fsr";

export async function load(_ctx: Ctx<"/sales">) {
  return {
    months: ["Jul", "Aug", "Sep"],
    units: [42, 57, 61],
  };
}
```

The page places the chart as a browser island, with no `mode`, because `useEffect` and a live canvas are exactly what server mode cannot do:

```tsx title="app/routes/sales/page.tsx"
import type { SalesProps } from "@generated/client";
import { Island } from "@snapfire/fsr-client/react";
import { Sales } from "@src/ui/Sales";

export default function SalesPage({ months, units }: SalesProps) {
  return (
    <section>
      <h1>Sales</h1>
      <Island when="visible">
        <Sales labels={months} values={units} />
      </Island>
    </section>
  );
}
```

`when="visible"` means the module isn't fetched until the chart scrolls into view. The page around it stays server-rendered.

## Where the types came from

```shell-session
$ fsr types shop/app
types     chart.js                     chart.js 4.5.1
kept      react
kept      react-dom
```

FSR pulled the declarations out of the published package and wrote them under `app/types/`. Those are for your editor and for `tsc` and are never shipped. `types/` is gitignored, so a fresh clone runs `fsr types` once.

## A package with no declarations

Try adding a package that publishes no usable declarations:

```shell-session
$ fsr add shop/app date-fns@4.1.0
added     date-fns                     date-fns/date-fns.bundle.mjs  8128 bytes

$ fsr types shop/app
missing   date-fns                     no `types` in the package and nothing on DefinitelyTyped

$ fsr build shop/app
no declarations for date-fns; run `fsr types`
```

The build refuses, because every package in the import map must have declarations, so an untyped import can't quietly become `any` across your codebase.

Either remove the package or declare the part of it you actually use. The second takes about four lines:

```ts title="app/types/date-fns/index.d.ts"
export declare function format(date: Date | number, pattern: string): string;
export declare function formatDistanceToNow(date: Date | number): string;
```

Save it as `app/types/date-fns/index.d.ts` and build again:

```shell-session
$ fsr build shop/app --verbose
types     chart.js               types/chart.js  chart.js 4.5.1
          date-fns               types/date-fns
typecheck tsc 7.0.2 from cache, clean
```

The `date-fns` row now has a path and no version, which marks those declarations as yours and not the registry's.

## Mark a library external

If a library should come from the page that embeds your component rather than from your bundle, mark it external:

```shell-session
$ fsr add shop/app my-widget@2.0.0 --external react
```

The bundle then imports `react` instead of carrying it. The embedding page supplies it.

Next up: [340. Add Google Analytics with a consent banner](https://snapfirers.com/fsr/learn/340-add-google-analytics-with-a-consent-banner).
