Using Apache ECharts with ReactJS and TypeScript: Server Side Rendering (SSR)
Tags: ssr, react, echarts, typescript
Published: 2023-10-19 07:11:17

### What is SSR?


[BairesDev](https://www.bairesdev.com/blog/server-side-rendering-react/) explains this well: Server Side Rendering (SSR) is a paradigm under which we _render web pages on the server before sending them to the client_.

It has both [pros](https://www.debugbear.com/blog/server-side-rendering#what-are-the-advantages-of-server-side-rendering) and [cons](https://www.debugbear.com/blog/server-side-rendering#are-there-disadvantages-to-server-side-rendering), but some use cases may well justify its adoption. For instance, better search engine indexability could be a huge selling point for SSR for some people.

Broadly speaking, whatever aligns well with your end objectives (CSR, SSR, or a hybrid approach) is fine. However, in this article, we mainly explore **how to render charts using Apache ECharts via SSR**.

### How does Apache ECharts help us in building SSR apps?


As a charting library, it offers built-in support for both CSR and SSR modes.

The visualization instances produced by Apache ECharts can easily be converted to PNG data URLs on the server side. Further, it exposes an [isomorphic](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) API to allow converting these instances to SVG strings as well.

Subsequently, these image strings can be rendered in the browser using the [`<img>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) element, allowing for server-side rendering (SSR) capability.

The following discussion focuses on building ReactJS SSR applications using **NextJS** but this approach should work as a blueprint for other frameworks too.

---

Before we dive into the SSR way of using ECharts, here's a quick refresher on the CSR approach. 

### CSR Snippets

If you pick the Canvas Renderer, here's how a CSR Scatter Plot component may look like. _If the following syntax feels alien, you may want to go through two other articles in this series first: [this](https://dev.to/manufac/using-apache-echarts-with-react-and-typescript-353k) and [this](https://dev.to/manufac/using-apache-echarts-with-react-and-typescript-optimizing-bundle-size-29l8)._

```ts
// Import necessary modules and types from "echarts"
import { ... } from "echarts/...";
import { CanvasRenderer } from "echarts/renderers";
import { useRef, useEffect } from "react";

use([
  ...,
  ...,
  CanvasRenderer,
]);

export interface Props {
  theme?: "light" | "dark";
  data: Record<string, number>[];
}

export function CanvasRendererScatterPlot({
  theme,
  data,
}: Props): JSX.Element {
  const chartRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const chart = init(chartRef.current, theme);

    return () => {
      chart?.dispose();
    };
  }, [theme]);

  useEffect(() => {
    if (chartRef.current !== null) {
      const chart = getInstanceByDom(chartRef.current);
      const option = {...};
      chart?.setOption(option, true);
    }
  }, [data, theme]);

  return (
    <div
      ref={chartRef}
      style={...}
    />
  );
}
```

But if you want to pick the SVG Renderer instead, the code is mostly the same except that instead of using `CanvasRenderer`, we will need to use `SVGRenderer`.

Now, moving onto the SSR approach:

### SSR Snippets

**A. Canvas**

```ts
import { ... } from "echarts/...";
import { CanvasRenderer } from "echarts/renderers";
import { Canvas, createCanvas } from "canvas";

use([
  ...,
  ...,
  CanvasRenderer,
]);

export function getCanvasScatterPlotServerSideProps(points: number): Canvas {
  const canvas = createCanvas(400, 500);
  const chart = init(canvas as unknown as HTMLCanvasElement);
  const option = {
    ...,
    series: {
      type: "scatter",
      encode: {
        x: "x",
        y: "y",
      },
      progressiveThreshold: points + 1,
    },
    ...
  };
  chart.setOption(option);

  return canvas;
}

```

The `dataURL` can be generated from the `Canvas` instance via `canvas.toDataURL()`. This data URL can be sent to the client which can then render the chart image as follows:

```ts
import Image from "next/image";

export function SSRCanvasRendererScatterPlot({ url }: { url: string }): JSX.Element {
  return (
    <div style={...}>
      <Image src={url} width={400} height={500} alt="SSR canvas scatter plot" />
    </div>
  );
}
```

**B. SVG**

```ts
import { ... } from "echarts/...";
import { SVGRenderer } from "echarts/renderers";

use([
  ...,
  ...,
  SVGRenderer,
]);


export function getSVGScatterPlotServerSideProps(points: number): string {
  const chart = init(null as unknown as HTMLElement, undefined, {
    renderer: "svg",
    ssr: true,
    width: 400,
    height: 500,
  });

  const option = {
    ...,
    animation: false,
    series: {
      type: "scatter",
      encode: {
        x: "x",
        y: "y",
      },
      progressiveThreshold: points + 1,
    },
    ...
  };

  chart.setOption(option);
  const chartData = chart.renderToSVGString();
  chart.dispose();
  return chartData;
}
```

Similar to Canvas, the SVG string can be sent to the client where it can be rendered as follows:

```ts
import Image from "next/image";

export function SSRSVGRendererScatterPlot({
  svgDataString,
}: {
  svgDataString: string;
}): JSX.Element {
  return (
    <div style={{ ... }}>
      <Image
        src={`data:image/svg+xml;utf8,${encodeURIComponent(svgDataString)}`}
        width={400}
        height={500}
        alt="SSR SVG scatter plot"
      />
    </div>
  );
}
```

Now that we know how to generate charts on server-side, we simply need a mechanism to fetch that data on the client-side for final rendering. The [API Routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) approach is showcased below. However, the [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components) approach should work fine as well.

```ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { getCanvasScatterPlotServerSideProps } from "@/components/SSRCanvasRendererScatterPlot";
import { getSVGScatterPlotServerSideProps } from "@/components/SSRSVGRendererScatterplot";
import { SSRScatterPlotData } from "@/components/utils";
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse<SSRScatterPlotData>) {
  const { points } = req.query;
  let ssrData: SSRScatterPlotData = { svgDataString: "", canvasDataURL: "" };
  ...
  ...
  const canvas = getCanvasScatterPlotServerSideProps(...);
  const svgDataString = getSVGScatterPlotServerSideProps(...);
  ssrData = { svgDataString, canvasDataURL: canvas.toDataURL() };
  res.status(200).json(ssrData);
}
```

And then, we can consume those image strings as shown below:

```tsx

export const getServerSideData = async (...) => {
  ...
  const rawRes = await fetch(`/api/...`, {
    method: "GET",
  });
  const { canvasDataURL, svgDataString } = (await rawRes.json());
  return {
    canvasDataURL,
    svgDataString,
  };
};

...
...

const [ssrData, setSSRData] = useState();

useEffect(() => {
  ...
  getServerSideData(...)
    .then((res) => {
      setSSRData(res);
    })
    .catch(console.log);
  setData(scatterPlotData);
}, [value]);

...
...

<Grid>
  <Grid.Col span={6} h="50vh">
    {ssrData?.canvasDataURL === undefined ? null : (
      <SSRCanvasRendererScatterPlot url={ssrData.canvasDataURL} />
    )}
  </Grid.Col>
  <Grid.Col span={6} h="50vh">
    {ssrData?.svgDataString === undefined ? null : (
      <SSRSVGRendererScatterPlot svgDataString={ssrData.svgDataString} />
    )}
  </Grid.Col>
</Grid>
```

---

### Parting Notes

1. Generating charts on server-side via ECharts is achievable, thanks to the in-built support that ECharts provide.
2. The approach involves creating a PNG or SVG string on the server side and then rendering that string on client side using the `<img>` element's `src` attribute.
3. Although ECharts provide support for animation in server-side rendered charts [by embedding CSS animations in the output SVG string](https://echarts.apache.org/handbook/en/how-to/cross-platform/server/#animations-in-server-side-rendering), those animations can still feel limited as compared to JS animations. 
4. While rendering a scatter plot on server-side via SVG renderer, we observed that all the points were getting rendered in 1 corner instead of their expected places. Disabling the animation fixed the issue.
5. Another thing that made us scratched our heads for a bit was the progressive rendering feature that ECharts provides. 

> Since ECharts 4, "progressive rendering" is supported in its workflow, which processes and renders data chunk by chunk alone with each frame, avoiding to block the UI thread of the browser. 

Although it can be desirable in CSR cases, for SSR it understandably led to partial rendering of the chart whenever the data exceeded the `progressiveThreshold`. We resolved this issue by always setting the `progressiveThreshold` to value greater than the size of the data so that progressive rendering doesn't trigger.