hugocxl / react-echarts

🐳 ECharts for React
https://hugocxl.github.io/react-echarts/
MIT License
69 stars 4 forks source link

Using narrowed type for props #18

Closed xsjcTony closed 5 months ago

xsjcTony commented 5 months ago

Description

The use prop is a really good wrapper to reduce bundle size: https://hugocxl.github.io/react-echarts/docs/bundle-size However, wondering if we have a way (e.g. generic) that can narrow down the prop type to those modules we included only? https://echarts.apache.org/handbook/en/basics/import#creating-an-option-type-in-typescript just like in the official docs

Problem Statement/Justification

For better type-checking in case of using in-exist props

Proposed Solution or API

Maybe generic?

Alternatives

No response

Additional Information

No response

hugocxl commented 5 months ago

Hi @xsjcTony, I'm currently exposing the parameters from the ECharts "use" function. Any ideas about how could we improve it?

https://github.com/hugocxl/react-echarts/blob/fa4288e288f9e72e5819a2905e229b8ed5d63762/src/use-echarts.ts#L18C1-L19C1

xsjcTony commented 5 months ago

Actually, (I didn't do any experiments yet) I might be able to define the type myself https://echarts.apache.org/handbook/en/basics/import#creating-an-option-type-in-typescript and do such following:

Since the composed ECOptions should always be the subtype of EChartsOptions (not 100% sure, but ideally since it's provided by echarts itself), so options are always passable to <ECharts />.

The only thing is that how we can narrow the prop type of CustomizedECharts, that we might need to introduce a generic type parameter for both useECharts and ECharts component, that would replace EChartsOptions, but I'm not too sure. By doing that, we may need to cast the type somewhere in the implementations, otherwise the destructuring against generic type will gonna be a nightmare. It's generally OK to cast so, since passing down undefined to init or setOptions is not harmful at all if some of options are missing.

If the below is working, it might worth to include in the documentation, at least we have a way to narrow the type ourselves without actually making any changes to this lib. Although it's kind of common sense to TS developers, but yeah, why not mention itπŸ˜„

export type ECOption = ComposeOption<
  | BarSeriesOption
  | LineSeriesOption
  | TitleComponentOption
  | TooltipComponentOption
  | GridComponentOption
  | DatasetComponentOption
>

const CustomizedECharts: FC<Omit<EChartProps, 'use'>> = (props) => {
  return <ECharts {...props} use={/* Modules to be used */} />
}

export default CustomizedECharts
const options: ECOption = {
  // ...
}

return <CustomizedECharts{...options} />
hugocxl commented 5 months ago

I love the idea, great suggestion. I've been trying to come up with a proposal but got some issues and not much time to work on this πŸ™, would you be up to creating a PR?

xsjcTony commented 5 months ago

Same here, but will take a look if time allows.

xsjcTony commented 5 months ago

Below is what I implemented myself (of course far from what you have done, but satisfies my needs), but it does not have the issues described in https://github.com/hugocxl/react-echarts/issues/19 and https://github.com/hugocxl/react-echarts/issues/20.

And maybe you can take the idea of another generic type to constraint option type to used modules only.

eCharts.ts:

import { useUpdateEffect } from 'ahooks';
import { init, use } from 'echarts/core';
import { useEffect, useRef } from 'react';
import { nextTick } from '@utils/dom';
import type { EChartsOption } from 'echarts';
import type { ECharts, EChartsCoreOption } from 'echarts/core';
import type { RefCallback } from 'react';

type UseEChartsOptions<Options extends EChartsCoreOption> = {
  options: Options;
  modules?: Parameters<typeof use>[0];
  theme?: Parameters<typeof init>[1];
  initOptions?: Parameters<typeof init>[2];
};

const useECharts = <
  T extends HTMLElement,
  Options extends EChartsCoreOption = EChartsOption,
>(params: UseEChartsOptions<Options>): RefCallback<T> => {

  const {
    modules,
    theme,
    initOptions,
    options,
  } = params;

  const containerRef = useRef<T | null>(null);
  const eChartsRef = useRef<ECharts | null>(null);

  const initChart = async (): Promise<void> => {
    if (!containerRef.current || eChartsRef.current)
      return;

    modules && use(modules);

    const eChartsInstance = init(containerRef.current, theme, initOptions);

    await nextTick();
    eChartsInstance.setOption(options);

    eChartsRef.current = eChartsInstance;
  };

  const disposeChart = (): void => {
    eChartsRef.current?.dispose();
    eChartsRef.current = null;
  };

  const containerRefCallback: RefCallback<T> = async (dom) => {
    if (containerRef.current && eChartsRef.current)
      return;

    containerRef.current = dom;
    await initChart();
  };

  useUpdateEffect(() => eChartsRef.current?.setOption(options), [options]);

  useEffect(() => {
    const resizeObserver = new ResizeObserver(() => eChartsRef.current?.resize());
    containerRef.current && resizeObserver.observe(containerRef.current);

    return () => {
      resizeObserver.disconnect();
      disposeChart();
      eChartsRef.current?.clear();
    };
  }, []);

  useUpdateEffect(() => {
    disposeChart();
    void initChart();
  }, [theme, initOptions]);

  return containerRefCallback;
};

export default useECharts;

ECharts.tsx:

/* eslint-disable ts/sort-type-constituents */

import { theme } from 'antd';
import { BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, TitleComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import useECharts from '@components/ECharts/hooks/eCharts';
import type { BarSeriesOption } from 'echarts/charts';
import type { GridComponentOption, TooltipComponentOption, TitleComponentOption } from 'echarts/components';
import type { ComposeOption, init } from 'echarts/core';
import type { FC } from 'react';

type EChartsInitOptions = NonNullable<Parameters<typeof init>[2]>;

export type EChartsOptions = ComposeOption<
  | BarSeriesOption
  | GridComponentOption
  | TooltipComponentOption
  | TitleComponentOption
>;

type EChartsProps = {
  options: EChartsOptions;
  width?: EChartsInitOptions['width'];
  height: NonNullable<EChartsInitOptions['height']>;
};

const ECharts: FC<EChartsProps> = ({
  options,
  width,
  height,
}) => {

  const { token: { colorPrimary } } = theme.useToken();

  const refCallback = useECharts<HTMLDivElement, EChartsOptions>({
    initOptions: { width, height },
    modules: [
      BarChart,
      GridComponent,
      TooltipComponent,
      TitleComponent,
      CanvasRenderer,
    ],
    options: {
      color: [colorPrimary],
      ...options,
    },
  });

  return <div ref={refCallback} />;
};

export default ECharts;