How to integrate plotly.js on Next.js 14 with App Router
Tags: webdev, nextjs, tutorial, plotly
Published: 2024-05-28 11:25:34

I remember struggling quite a bit with integrating the plotly.js library into Next.js in the past.
However, now with Next.js 14 and the use of app routers, I've had to do it all over again, but this time the pain was quite painful.
Eventually I got over it, and today I'm going to share my experience with you, along with a few tips on how to integrate plotly.js into next.js.

Add the plotly.js library to your existing environment.

## Install plotly.js

```sh
npm i -S plotly.js
# or
yarn add plotly.js
# or
pnpm add plotly.js
```

If you're using Typescript, you can't forget about type definitions.

```sh
npm i -D @types/plotly.js
```

If you felt something strange, yes, you're right, plotly.js has a React wrapper. But, it hasn't been updated anymore since 2 years ago, and I don't install it because I personally felt a lot of discomfort in the Typescript environment when I first developed it.
If you still want to install it, you can do so. However, I'm going to skip the React wrapper part and create a very simple wrapper component.

## Use plotly.js

Of course, the plotly.js library is completely browser-specific, so build errors will be waiting for you the moment you import it and start using it.
So, the react-plotly's issue suggests using it as follows:

```js
import dynamic from 'next/dynamic'

export const Plotly = dynamic(() => import('react-plotly.js'), { ssr: false });
```

The reason why the react-plotly component doesn't work despite the `'use client'` directive is because it's a class component, and it was designed from the ground up assuming a browser. As if that weren't bad enough, now that I'm not touching anything but JS libraries, I'm starting to feel self-conscious about why I'm using this when there are actually better charting libraries out there.

Now, let's get back to business. Here's the simple wrapper component I created.

```jsx
export const Plotly = dynamic(
  () =>
    import('plotly.js/dist/plotly.js').then(({ newPlot, purge }) => {
      const Plotly = forwardRef(({ id, className, data, layout, config }, ref) => {
        const originId = useId();
        const realId = id || originId;
        const originRef = useRef(null);
        const [handle, setHandle] = useState(undefined);

        useEffect(() => {
          let instance;
          originRef.current &&
            newPlot(originRef.current!, data, layout, config).then((ref) => setHandle((instance = ref)));
          return () => {
            instance && purge(instance);
          };
        }, [data]);

        useImperativeHandle(
          ref,
          () => (handle ?? originRef.current ?? document.createElement('div')),
          [handle]
        );

        return <div id={realId} ref={originRef} className={className}></div>;
      });
      Plotly.displayName = 'Plotly';
      return Plotly;
    }),
  { ssr: false }
);
```

Is the import path weird? No, this is the import path used by the [react-plotly source](https://github.com/plotly/react-plotly.js/blob/master/src/react-plotly.js#L2).

Anyway, apply the component like this, and you're done.

```jsx
<div>
  <Plotly
    style={{ width: '640px', height: '480px' }}
    data={[{ x: [1, 2, 3, 4, 5], y: [1, 2, 4, 8, 16] }]}
    layout={{ margin: { t: 0 } }}
  />
</div>
```

But if you're using Typescript, there's still one problem.

## Make typescript friendly

If you specify an import path like `import('plotly.js/dist/plotly.js')`, Typescript will fail to import the type. The reason for this is simple. It's because the type definition is based on the default path, `import('plotly.js')`.

There are two ways to fix the problem.

1. Create a `d.ts` file that duplicates the default types in the path `'plotly.js/dist/plotly.js'`.
2. include webpack resolve alias to bypass it (included when using turbopack)

I used method #2 because it also solved the build error.

Now let's touch the `next.config.js` file, which in my case was based on `next.config.mjs`.

```js
import path from 'node:path';

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  output: 'standalone',
  images: {},
  ...bundleOptions(),
};

export default nextConfig;

function bundleOptions() {
  const resolvers = {
    'plotly.js': 'plotly.js/dist/plotly.js'
  }

  if (process.env.TURBOPACK) // If you are using dev with --turbo
    return {
      experimental: {
        turbo: {
          resolveAlias: {
            ...resolvers
          }
        },
      },
    };
  else return { // otherwise, for webpack
    webpack: (config) => {
      for (const [dep, to] of Object.entries(resolvers))
        config.resolve.alias[dep] = path.resolve(
          config.context,
          to
        );
      return config;
    },
  };
}

```

Now you can modify the import path in the wrapper component as you would normally import it.

```diff
-     import('plotly.js/dist/plotly.js').then(({ newPlot, purge }) => {
+     import('plotly.js').then(({ newPlot, purge }) => {
```

Then, fix the required types for typescript, and voila!

Done. Congratulations. You can now use plotly.js in Next.js!

## Conculusion

I chose plotly.js over several popular charting libraries, especially the feature-rich Apache ECharts, because my company's algorithmic research team uses Python, and the plotly Python library is available, which is an advantage for exchanging chart data and layouts.

I hope I'm not wrong in my choice for this web application product development. lol

Happy Next.js-ing~!