源码共读 | react-use 源码

1.源码介绍

2.准备工作

下载源码

bash 复制代码
git clone https://github.com/streamich/react-use

# 可以下载若川大佬的仓库
git clone https://github.com/lxchuan12/react-use-analysis.git

安装依赖及运行

bash 复制代码
yarn install
yarn start

运行成功后会自动打开 Storybook 服务,也就是图中的 http://localhost:6008/

可以看到左侧的目录中既有文档,又有 Demo

3.开始阅读

下面正式开始跟着 若川大佬 和官方文档进行源码阅读。

3.1 useEffectOnce

这个 Hook 的源码非常简单,只有 7 行:

js 复制代码
import { EffectCallback, useEffect } from 'react';

const useEffectOnce = (effect: EffectCallback) => {
  useEffect(effect, []);
};

export default useEffectOnce;

看到这个源码我们很容易知道它的作用:只运行一次 React 的 useEffect 生命周期

事实上我的代码里很多useEffect(() => {/* my code */}, []);,几乎每个组件里都要写一次,但是真没想到这么简短的也能封装一下,真是把封装做到了极致。

3.1.1 测试用例

项目里提供了非常丰富的测试用例,下面先跟着测试用例看看。 要调试测试用例,需要先在 Vscode 中安装 JestJest Runner 插件,安装完成后,可测试的测试用例上面会有 Run 按钮:

动图运行如下:

定格到最后一帧,我们可以看到,输出和预期是一致的:

3.1.2 Sotrybook Demo

除了在代码中直接运行测试用例外,前面跑起来的 Storybook 服务中也提供了 Demo,可以直观地看到结果:

我们可以看到当我们进入 useEffectOnceDemo 中的时候,控制台输出了 Docs 中 Usage 中的 console.log('Running effect once on mount'),离开 Demo 中的时候又运行了console.log('Running clean-up of effect on unmount'),均符合预期。

3.2 Sensors

从官方文档的左侧边栏可以看出,所有的 Hook 被分成了SensorsStateSide EffectsUILifecycleMiscellaneousAnimation这七大类,我们先来看Sensors

"Sensor Hooks" listen to changes in some interface and force your components to be re-rendered with the new state, up-to-date state. 监听某些界面的变化,并强制组件以新状态、最新状态重新渲染组件

Sensors是传感器的意思,主要的功能是监听用户和系统的行为 ,用户行为包括鼠标(useMouse)、滚轮(useScroll)、键盘(useKey)、输入(useStartTyping)等行为,系统行为包括电池状态(useBattery)、Window 尺寸(useWindowSize)、用户是否活跃(useIdle)等。

下面以用户行为和系统行为为例,各详细演示一个。

3.2.1 useMouse

先上源码:

js 复制代码
import { RefObject, useEffect } from 'react';

import useRafState from './useRafState';
import { off, on } from './misc/util';

export interface State {
  docX: number;
  docY: number;
  posX: number;
  posY: number;
  elX: number;
  elY: number;
  elH: number;
  elW: number;
}

const useMouse = (ref: RefObject<Element>): State => {
  if (process.env.NODE_ENV === 'development') {
    if (typeof ref !== 'object' || typeof ref.current === 'undefined') {
      console.error('useMouse expects a single ref argument.');
    }
  }

  const [state, setState] = useRafState<State>({
    docX: 0,
    docY: 0,
    posX: 0,
    posY: 0,
    elX: 0,
    elY: 0,
    elH: 0,
    elW: 0,
  });

  useEffect(() => {
    const moveHandler = (event: MouseEvent) => {
      if (ref && ref.current) {
        const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
        const posX = left + window.pageXOffset;
        const posY = top + window.pageYOffset;
        const elX = event.pageX - posX;
        const elY = event.pageY - posY;

        setState({
          docX: event.pageX,
          docY: event.pageY,
          posX,
          posY,
          elX,
          elY,
          elH,
          elW,
        });
      }
    };

    on(document, 'mousemove', moveHandler);

    return () => {
      off(document, 'mousemove', moveHandler);
    };
  }, [ref]);

  return state;
};

export default useMouse;

使用的 Demo:

js 复制代码
import {useMouse} from 'react-use';

const Demo = () => {
  const ref = React.useRef(null);
  const {docX, docY, posX, posY, elX, elY, elW, elH} = useMouse(ref);

  return (
    <div ref={ref}>
      <div>Mouse position in document - x:{docX} y:{docY}</div>
      <div>Mouse position in element - x:{elX} y:{elY}</div>
      <div>Element position- x:{posX} y:{posY}</div>
      <div>Element dimensions - {elW}x{elH}</div>
    </div>
  );
};

效果展示:

可以看到,这个 hook 的使用非常简单,只要传入一个元素的 ref 就能够返回鼠标在这个元素内部的各种信息(主要是位置信息)。

另外比较神奇的是,这些参数(docXdocY等)都具有 state 的响应性,这里之所以能做到这点,主要是用了 useRafState,项目里也有源码,这里不再赘述。

3.2.2 useBattery

这个 hook 是检测电池状态的,手机上我没有试过,但是电脑上,确实有用!

源码:

js 复制代码
import { useEffect, useState } from 'react';
import { isNavigator, off, on } from './misc/util';
import isDeepEqual from './misc/isDeepEqual';

export interface BatteryState {
  charging: boolean;
  chargingTime: number;
  dischargingTime: number;
  level: number;
}

interface BatteryManager extends Readonly<BatteryState>, EventTarget {
  onchargingchange: () => void;
  onchargingtimechange: () => void;
  ondischargingtimechange: () => void;
  onlevelchange: () => void;
}

interface NavigatorWithPossibleBattery extends Navigator {
  getBattery?: () => Promise<BatteryManager>;
}

type UseBatteryState =
  | { isSupported: false } // Battery API is not supported
  | { isSupported: true; fetched: false } // battery API supported but not fetched yet
  | (BatteryState & { isSupported: true; fetched: true }); // battery API supported and fetched

const nav: NavigatorWithPossibleBattery | undefined = isNavigator ? navigator : undefined;
const isBatteryApiSupported = nav && typeof nav.getBattery === 'function';

function useBatteryMock(): UseBatteryState {
  return { isSupported: false };
}

function useBattery(): UseBatteryState {
  const [state, setState] = useState<UseBatteryState>({ isSupported: true, fetched: false });

  useEffect(() => {
    let isMounted = true;
    let battery: BatteryManager | null = null;

    const handleChange = () => {
      if (!isMounted || !battery) {
        return;
      }
      const newState: UseBatteryState = {
        isSupported: true,
        fetched: true,
        level: battery.level,
        charging: battery.charging,
        dischargingTime: battery.dischargingTime,
        chargingTime: battery.chargingTime,
      };
      !isDeepEqual(state, newState) && setState(newState);
    };

    nav!.getBattery!().then((bat: BatteryManager) => {
      if (!isMounted) {
        return;
      }
      battery = bat;
      on(battery, 'chargingchange', handleChange);
      on(battery, 'chargingtimechange', handleChange);
      on(battery, 'dischargingtimechange', handleChange);
      on(battery, 'levelchange', handleChange);
      handleChange();
    });

    return () => {
      isMounted = false;
      if (battery) {
        off(battery, 'chargingchange', handleChange);
        off(battery, 'chargingtimechange', handleChange);
        off(battery, 'dischargingtimechange', handleChange);
        off(battery, 'levelchange', handleChange);
      }
    };
  }, []);

  return state;
}

export default isBatteryApiSupported ? useBattery : useBatteryMock;

Demo及演示:

js 复制代码
import {useBattery} from 'react-use';

const Demo = () => {
  const batteryState = useBattery();

  if (!batteryState.isSupported) {
    return (
      <div>
        <strong>Battery sensor</strong>: <span>not supported</span>
      </div>
    );
  }

  if (!batteryState.fetched) {
    return (
      <div>
        <strong>Battery sensor</strong>: <span>supported</span> <br />
        <strong>Battery state</strong>: <span>fetching</span>
      </div>
    );
  }

  return (
    <div>
      <strong>Battery sensor</strong>:&nbsp;&nbsp; <span>supported</span> <br />
      <strong>Battery state</strong>: <span>fetched</span> <br />
      <strong>Charge level</strong>:&nbsp;&nbsp; <span>{ (batteryState.level * 100).toFixed(0) }%</span> <br />
      <strong>Charging</strong>:&nbsp;&nbsp; <span>{ batteryState.charging ? 'yes' : 'no' }</span> <br />
      <strong>Charging time</strong>:&nbsp;&nbsp;
      <span>{ batteryState.chargingTime ? batteryState.chargingTime : 'finished' }</span> <br />
      <strong>Discharging time</strong>:&nbsp;&nbsp; <span>{ batteryState.dischargingTime }</span>
    </div>
  );
};

图中之所以会变化是因为我拔了电源,然后又插了回去,所以Charging 从 Yes 变成 No,又变回 Yes,不过我没有试过移动端,理论上也是可以做到监听的,有有试过的小伙伴们可以在评论区说一下。

看源代码这个功能主要是通过 window.navigator.getBattery() 实现的,之前还真不知道 navigator 下还有这么个功能,于是又去详细了解了一下 navigator 下的属性和方法,感兴趣的小伙伴也可以看看:Navigator - Web API 接口参考 | MDN (mozilla.org)

Senors 下的 Hook 就分享到这,下面看 State

3.3 State

"State Hooks" allow you to easily manage state of booleans, arrays, and maps. 允许轻松管理 boolean、array 和 map 的 state

3.3.1 useFirstMountState

这个 Hook 主要用来判断组件是否是初次渲染

源码非常简单,useRef 一般有两种用法,一种是作为 DOM 的引用,一种是作为数据的引用,使被引用对象在组件变化的时候保持不变。

js 复制代码
import { useRef } from 'react';

export function useFirstMountState(): boolean {
  const isFirst = useRef(true);

  if (isFirst.current) {
    isFirst.current = false;

    // 直接在初次渲染的时候返回 true,主打一个简单粗暴。
    return true;
  }

  return isFirst.current;
}

Demo

javascript 复制代码
import * as React from 'react';
import { useFirstMountState } from 'react-use';

const Demo = () => {
  const isFirstMount = useFirstMountState();
  const update = useUpdate();

  return (
    <div>
      <span>This component is just mounted: {isFirstMount ? 'YES' : 'NO'}</span>
      <br />
      <button onClick={update}>re-render</button>
    </div>
  );
};

由于比较简单,这里就不录制 Gif 动图了,可以直接查看演示:useFirstMountState - Demo

3.3.2 useList

监听一个数组并返回操作数组的方法,相比起默认的数组,通过这个 Hook 返回的方法操作数组可以触发 re-render

这个源码多一些,但是原理也很简单,所有的操作最后都转换成 set 方法了,而 set 方法中调用了另一个 useUpdate Hook,通过这个 Hook 触发更新。

js 复制代码
import { useMemo, useRef } from 'react';
import useUpdate from './useUpdate';
import { IHookStateInitAction, IHookStateSetAction, resolveHookState } from './misc/hookState';

export interface ListActions<T> {
  set: (newList: IHookStateSetAction<T[]>) => void;
  push: (...items: T[]) => void;
  updateAt: (index: number, item: T) => void;
  insertAt: (index: number, item: T) => void;
  update: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
  updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
  upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => void;
  sort: (compareFn?: (a: T, b: T) => number) => void;
  filter: (callbackFn: (value: T, index?: number, array?: T[]) => boolean, thisArg?: any) => void;
  removeAt: (index: number) => void;
  remove: (index: number) => void;
  clear: () => void;
  reset: () => void;
}

function useList<T>(initialList: IHookStateInitAction<T[]> = []): [T[], ListActions<T>] {
  const list = useRef(resolveHookState(initialList));
  const update = useUpdate();

  const actions = useMemo<ListActions<T>>(() => {
    const a = {
      set: (newList: IHookStateSetAction<T[]>) => {
        list.current = resolveHookState(newList, list.current);
        // 触发更新
        update();
      },

      push: (...items: T[]) => {
        items.length && actions.set((curr: T[]) => curr.concat(items));
      },

      updateAt: (index: number, item: T) => {
        actions.set((curr: T[]) => {
          const arr = curr.slice();

          arr[index] = item;

          return arr;
        });
      },

      insertAt: (index: number, item: T) => {
        actions.set((curr: T[]) => {
          const arr = curr.slice();

          index > arr.length ? (arr[index] = item) : arr.splice(index, 0, item);

          return arr;
        });
      },

      update: (predicate: (a: T, b: T) => boolean, newItem: T) => {
        actions.set((curr: T[]) => curr.map((item) => (predicate(item, newItem) ? newItem : item)));
      },

      updateFirst: (predicate: (a: T, b: T) => boolean, newItem: T) => {
        const index = list.current.findIndex((item) => predicate(item, newItem));

        index >= 0 && actions.updateAt(index, newItem);
      },

      upsert: (predicate: (a: T, b: T) => boolean, newItem: T) => {
        const index = list.current.findIndex((item) => predicate(item, newItem));

        index >= 0 ? actions.updateAt(index, newItem) : actions.push(newItem);
      },

      sort: (compareFn?: (a: T, b: T) => number) => {
        actions.set((curr: T[]) => curr.slice().sort(compareFn));
      },

      filter: <S extends T>(
        callbackFn: (value: T, index: number, array: T[]) => value is S,
        thisArg?: any
      ) => {
        actions.set((curr: T[]) => curr.slice().filter(callbackFn, thisArg));
      },

      removeAt: (index: number) => {
        actions.set((curr: T[]) => {
          const arr = curr.slice();

          arr.splice(index, 1);

          return arr;
        });
      },

      clear: () => {
        actions.set([]);
      },

      reset: () => {
        actions.set(resolveHookState(initialList).slice());
      },
    };

    /**
     * @deprecated Use removeAt method instead
     */
    (a as ListActions<T>).remove = a.removeAt;

    return a as ListActions<T>;
  }, []);

  return [list.current, actions];
}

export default useList;

Demo:State / useList - Demo ⋅ Storybook (streamich.github.io)

css 复制代码
import {useList} from 'react-use';

const Demo = () => {
  const [list, { set, push, updateAt, insertAt, update, updateFirst, upsert, sort, filter, removeAt, clear, reset }] = useList([1, 2, 3, 4, 5]);

  return (
    <div>
      <button onClick={() => set([1, 2, 3])}>Set to [1, 2, 3]</button>
      <button onClick={() => push(Date.now())}>Push timestamp</button>
      <button onClick={() => updateAt(1, Date.now())}>Update value at index 1</button>
      <button onClick={() => remove(1)}>Remove element at index 1</button>
      <button onClick={() => filter(item => item % 2 === 0)}>Filter even values</button>
      <button onClick={() => sort((a, b) => a - b)}>Sort ascending</button>
      <button onClick={() => sort((a, b) => b - a)}>Sort descending</button>
      <button onClick={clear}>Clear</button>
      <button onClick={reset}>Reset</button>
      <pre>{JSON.stringify(list, null, 2)}</pre>
    </div>
  );
};

3.4 Side-Effect

"Side-effect Hooks" allow your app trigger various side-effects using browser's API. 允许应用程序使用浏览器的API触发各种副作用

3.4.1 useAsyncFn

为异步函数或 Promise 函数返回状态和回调的 React 钩子,主要是将请求的 loadingerrorvalue 封装在钩子里并让它们成为响应式数据,当请求完成状态改变后自动触发 re-render

js 复制代码
import { DependencyList, useCallback, useRef, useState } from 'react';
import useMountedState from './useMountedState';
import { FunctionReturningPromise, PromiseType } from './misc/types';

export type AsyncState<T> =
  | {
      loading: boolean;
      error?: undefined;
      value?: undefined;
    }
  | {
      loading: true;
      error?: Error | undefined;
      value?: T;
    }
  | {
      loading: false;
      error: Error;
      value?: undefined;
    }
  | {
      loading: false;
      error?: undefined;
      value: T;
    };

type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> = AsyncState<
  PromiseType<ReturnType<T>>
>;

export type AsyncFnReturn<T extends FunctionReturningPromise = FunctionReturningPromise> = [
  StateFromFunctionReturningPromise<T>,
  T
];

export default function useAsyncFn<T extends FunctionReturningPromise>(
  fn: T,
  deps: DependencyList = [],
  initialState: StateFromFunctionReturningPromise<T> = { loading: false }
): AsyncFnReturn<T> {
  const lastCallId = useRef(0);
  const isMounted = useMountedState();
  const [state, set] = useState<StateFromFunctionReturningPromise<T>>(initialState);

  const callback = useCallback((...args: Parameters<T>): ReturnType<T> => {
    const callId = ++lastCallId.current;

    if (!state.loading) {
      set((prevState) => ({ ...prevState, loading: true }));
    }

    return fn(...args).then(
      (value) => {
        isMounted() && callId === lastCallId.current && set({ value, loading: false });

        return value;
      },
      (error) => {
        isMounted() && callId === lastCallId.current && set({ error, loading: false });

        return error;
      }
    ) as ReturnType<T>;
  }, deps);

  return [state, callback as unknown as T];
}

Demo:可以看到,state 中自带了 loadingerrorvalue 三个属性

js 复制代码
import {useAsyncFn} from 'react-use';

const Demo = ({url}) => {
  const [state, doFetch] = useAsyncFn(async () => {
    const response = await fetch(url);
    const result = await response.text();
    return result
  }, [url]);

  return (
    <div>
      {state.loading
        ? <div>Loading...</div>
        : state.error
          ? <div>Error: {state.error.message}</div>
          : <div>Value: {state.value}</div>
      }
      <button onClick={() => doFetch()}>Start loading</button>
    </div>
  );
};

3.4.2 useAsync

返回一个 Promise 或者一个异步函数,源码比较短,主要是因为使用了 useAsyncFn,它比起 useAsyncFn 多了主动调用异步函数这一步,相当于再封装了一层,使用起来更方便:

js 复制代码
import { DependencyList, useEffect } from 'react';
import useAsyncFn from './useAsyncFn';
import { FunctionReturningPromise } from './misc/types';

export { AsyncState, AsyncFnReturn } from './useAsyncFn';

export default function useAsync<T extends FunctionReturningPromise>(
  fn: T,
  deps: DependencyList = []
) {
  const [state, callback] = useAsyncFn(fn, deps, {
    loading: true,
  });

  useEffect(() => {
    callback();
  }, [callback]);

  return state;
}

Demo:

js 复制代码
import {useAsync} from 'react-use';

const Demo = ({url}) => {
  const state = useAsync(async () => {
    const response = await fetch(url);
    const result = await response.text();
    return result
  }, [url]);

  return (
    <div>
      {state.loading
        ? <div>Loading...</div>
        : state.error
          ? <div>Error: {state.error.message}</div>
          : <div>Value: {state.value}</div>
      }
    </div>
  );
};

3.4.3 useDebounce & useThrottle

防抖和节流,无论是面试还是平时,相信大家都接触得比较多,甚至都可以手写,就不过多介绍了,这里本质也是通过 setTimeout 实现的。

3.5 UI

"UI Hooks" allow you to control and subscribe to state changes of UI elements. 允许控制和订阅 UI 元素的状态更改

3.5.1 useCss

css 样式直接转换成 className,这个之前还挺困扰我的,每次都是写一长串 style 在元素上,有这个就方便多了。源码实现上的话,主要是通过 nano-css 库实现的:

js 复制代码
import { create, NanoRenderer } from 'nano-css';
import { addon as addonCSSOM, CSSOMAddon } from 'nano-css/addon/cssom';
import { addon as addonVCSSOM, VCSSOMAddon } from 'nano-css/addon/vcssom';
import { cssToTree } from 'nano-css/addon/vcssom/cssToTree';
import { useMemo } from 'react';
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect';

type Nano = NanoRenderer & CSSOMAddon & VCSSOMAddon;
const nano = create() as Nano;
addonCSSOM(nano);
addonVCSSOM(nano);

let counter = 0;

const useCss = (css: object): string => {
  const className = useMemo(() => 'react-use-css-' + (counter++).toString(36), []);
  const sheet = useMemo(() => new nano.VSheet(), []);

  useIsomorphicLayoutEffect(() => {
    const tree = {};
    cssToTree(tree, css, '.' + className, '');
    sheet.diff(tree);

    return () => {
      sheet.diff({});
    };
  });

  return className;
};

export default useCss;

Demo:

js 复制代码
import {useCss} from 'react-use';

const Demo = () => {
  const className = useCss({
    color: 'red',
    border: '1px solid red',
    '&:hover': {
      color: 'blue',
    },
  });

  return (
    <div className={className}>
      Hover me!
    </div>
  );
};

3.6 Lifecycles

"Lifecycle Hooks" modify and extend built-in React hooks or imitate React Class component lifecycle patterns. 修改和扩展了内置的 React Hook 并且模仿 React Class 组件的生命周期模式

3.6.1 useLifecycles

在 React mountunmount 生命周期分别调用不同的回调,主要是通过 useEffect 和对应的 cleanUp 实现的:

js 复制代码
import { useEffect } from 'react';

const useLifecycles = (mount, unmount?) => {
  useEffect(() => {
    if (mount) {
      mount();
    }
    return () => {
      if (unmount) {
        unmount();
      }
    };
  }, []);
};

export default useLifecycles;

Demo:

js 复制代码
import {useLifecycles} from 'react-use';

const Demo = () => {
  useLifecycles(() => console.log('MOUNTED'), () => console.log('UNMOUNTED'));
  return null;
};

3.6.2 usePromise

返回一个包含 Promise 函数的辅助函数,这个辅助函数会在组件 mount 的时候调用:

js 复制代码
import { useCallback } from 'react';
import useMountedState from './useMountedState';

export type UsePromise = () => <T>(promise: Promise<T>) => Promise<T>;

const usePromise: UsePromise = () => {
  const isMounted = useMountedState();
  return useCallback(
    (promise: Promise<any>) =>
      new Promise<any>((resolve, reject) => {
        const onValue = (value) => {
          isMounted() && resolve(value);
        };
        const onError = (error) => {
          isMounted() && reject(error);
        };
        promise.then(onValue, onError);
      }),
    []
  );
};

export default usePromise;

Demo:

js 复制代码
import {usePromise} from 'react-use';

const Demo = ({promise}) => {
  const mounted = usePromise();
  const [value, setValue] = useState();

  useEffect(() => {
    (async () => {
      const value = await mounted(promise);
      // This line will not execute if <Demo> component gets unmounted.
      setValue(value);
    })();
  });
};

3.7 Animation

"Animation Hooks" usually interpolate numeric values over time.通常随时间插入数值(一些简单的数字动画效果)

3.7.1 useSpring

根据弹簧动力学随时间更新单个数值,就是常见的数字滚动增加、减少的效果,需要强调的是,这里用了一个 rebound 第三方库:

js 复制代码
import { useEffect, useMemo, useState } from 'react';
import { Spring, SpringSystem } from 'rebound';

const useSpring = (targetValue: number = 0, tension: number = 50, friction: number = 3) => {
  const [spring, setSpring] = useState<Spring | null>(null);
  const [value, setValue] = useState<number>(targetValue);

  // memoize listener to being able to unsubscribe later properly, otherwise
  // listener fn will be different on each re-render and wouldn't unsubscribe properly.
  const listener = useMemo(
    () => ({
      onSpringUpdate: (currentSpring) => {
        const newValue = currentSpring.getCurrentValue();
        setValue(newValue);
      },
    }),
    []
  );

  useEffect(() => {
    if (!spring) {
      const newSpring = new SpringSystem().createSpring(tension, friction);
      newSpring.setCurrentValue(targetValue);
      setSpring(newSpring);
      newSpring.addListener(listener);
    }

    return () => {
      if (spring) {
        spring.removeListener(listener);
        setSpring(null);
      }
    };
  }, [tension, friction, spring]);

  useEffect(() => {
    if (spring) {
      spring.setEndValue(targetValue);
    }
  }, [targetValue]);

  return value;
};

export default useSpring;

Demo:

js 复制代码
import useSpring from 'react-use/lib/useSpring';

const Demo = () => {
  const [target, setTarget] = useState(50);
  const value = useSpring(target);

  return (
    <div>
      {value}
      <br />
      <button onClick={() => setTarget(0)}>Set 0</button>
      <button onClick={() => setTarget(100)}>Set 100</button>
    </div>
  );
};

本期源码收获

至此,我们就简单分析了一下 react-use 源码各个模块的几个 Hook,虽然整体难度不大,但是数量还是挺多的,感兴趣的小伙伴就赶紧源码看起来吧,我可以你肯定也可以的 :fist:

学完之后的实战使用

Echarts 想来大家都用过,确实好用,尤其是 5.0 版本更新之后支持了按需导入,不仅好用,对项目负担也不大。但有点不好的是,每次使用都需要 echarts.use 一大堆,导包也需要导入一大堆,如下:

js 复制代码
import { PieChart } from 'echarts/charts';
import * as echarts from 'echarts/core';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { LegendComponent } from 'echarts/components';

import { OverviewText } from '@/crm/components/Common/CRMStyle';

echarts.use([LegendComponent, PieChart, CanvasRenderer, LabelLayout]);

学习完这次源码之后正好可以通过自定义 Hook 将这一大堆重复代码封装起来,经过一通操作,最后封装了一个 useEcharts,代码如下:

js 复制代码
import { useEffect, useState } from 'react';
import * as echarts from 'echarts/core';
import {
  ToolboxComponent,
  TooltipComponent,
  GridComponent,
  LegendComponent,
} from 'echarts/components';
import { BarChart, LineChart } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
  ToolboxComponent,
  TooltipComponent,
  GridComponent,
  LegendComponent,
  BarChart,
  LineChart,
  CanvasRenderer,
  UniversalTransition,
]);

const useEcharts = (chartRef) => {
  const [initedChart, setInitedChart] = useState(null);

  useEffect(() => {
    if (chartRef) {
      setInitedChart(echarts.init(chartRef.current));
    }
  }, [chartRef]);

  return initedChart;
};

export default useEcharts;

好啦,大家也赶紧学起来、用起来吧,下一篇源码篇见~

如有错误,烦请指正,感谢大家~

相关推荐
在云端易逍遥1 分钟前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare2 分钟前
选择文件夹路径
前端
艾小码3 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月3 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁7 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅7 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸9 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端
我是日安9 分钟前
从零到一打造 Vue3 响应式系统 Day 9 - Effect:调度器实现与应用
前端·vue.js
Mintopia21 分钟前
🚀 Next.js 全栈 E2E 测试:Playwright vs Cypress
前端·javascript·next.js