From 2a52201fb0c0f4bd998f2426c47ee55bfec7202d Mon Sep 17 00:00:00 2001 From: garfieldduck Date: Tue, 26 Feb 2019 17:06:46 +0800 Subject: [PATCH 1/5] Rewrite HoverLayer using Hooks --- packages/graph/src/layers/HoverLayer.tsx | 156 +++++++++++++---------- 1 file changed, 92 insertions(+), 64 deletions(-) diff --git a/packages/graph/src/layers/HoverLayer.tsx b/packages/graph/src/layers/HoverLayer.tsx index 4ff3c07..8b0038c 100644 --- a/packages/graph/src/layers/HoverLayer.tsx +++ b/packages/graph/src/layers/HoverLayer.tsx @@ -1,4 +1,9 @@ -import * as React from 'react'; +import React, { + FunctionComponent, + useRef, + useEffect, + useCallback, +} from 'react'; import { throttle } from 'lodash-es'; import { localPoint } from '@vx/event'; @@ -14,72 +19,95 @@ export interface HoverLayerProps { /** Function to hide the tooltip */ clearHovering: () => void; - /** Hidden components to detect the mouse or touch interactions */ + /** + * Hidden components to detect the mouse or touch interactions. + * **Note:** The order of the components should correspond to the order of the data. + */ collisionComponents: JSX.Element[]; - /** The debounce time for the mouse and touch events */ + /** The throttle time for the mouse and touch events */ throttleTime: number; } -export class HoverLayer extends React.PureComponent { - public static defaultProps = { - throttleTime: 180, - handleHover: () => null, - }; - - public animaFrameID: number; - - /** Updates the position of the tooltip and sets the currently active data index */ - private updatePosition = (dataIndex: number, event: React.SyntheticEvent) => { - const { setHoveredPosAndIndex, handleHover } = this.props; - - // custom action which executes before the position is updated - handleHover(); - - // convert the position of the event to the coordinate system of the SVG - const { x, y } = localPoint(event); - this.animaFrameID = window.requestAnimationFrame(() => { - setHoveredPosAndIndex( - dataIndex, - x, - y, - ); - }); - }; - - private throttledUpdatePosition = throttle(this.updatePosition, this.props.throttleTime); - - private handleTooltip = (dataIndex: number) => (event: React.SyntheticEvent) => { - // removes it from the event pool and allows references to the event - event.persist(); - this.throttledUpdatePosition(dataIndex, event); - }; - - private hideTooltip = () => { - // cancel the previously thorttled event to prevent the tooltip from reappearing - this.throttledUpdatePosition.cancel(); - this.props.clearHovering(); - }; - - public componentWillUnmount() { - window.cancelAnimationFrame(this.animaFrameID); - } - - public render() { - const { collisionComponents } = this.props; - const detectionAreas = collisionComponents.map((area: JSX.Element, dataIndex: number) => { - const handleCurrentTooltip = this.handleTooltip(dataIndex); - return React.cloneElement(area, { - onTouchStart: handleCurrentTooltip, - onTouchMove: handleCurrentTooltip, - onMouseMove: handleCurrentTooltip, - onMouseLeave: this.hideTooltip, - }); +export const HoverLayer: FunctionComponent = ({ + setHoveredPosAndIndex, + handleHover= () => null, + clearHovering, + collisionComponents, + throttleTime = 180, +}) => { + /** stores the animation frame ID of the scheduled update of hovered position and data index */ + const animaFrameIDRef = useRef(null); + + /** Function to update the position of the tooltip and sets the currently active data index */ + const updatePosition = useCallback( + throttle( + (dataIndex: number, event: React.SyntheticEvent) => { + // custom action which executes before the position is updated + handleHover(); + + // convert the position of the event to the coordinate system of the SVG + const { x, y } = localPoint(event); + animaFrameIDRef.current = window.requestAnimationFrame(() => { + setHoveredPosAndIndex( + dataIndex, + x, + y, + ); + }); + }, + throttleTime, + ), + [], + ); + + /** Function to keep the event data and perform throttled updates of the position */ + const handleTooltip = useCallback( + (dataIndex: number) => (event: React.SyntheticEvent) => { + // removes it from the event pool and allows references to the event + event.persist(); + updatePosition(dataIndex, event); + }, + [], + ); + + /** Function to cancel the update of position and disable the hovering state */ + const hideTooltip = useCallback( + () => { + // cancel the previously thorttled event to prevent the tooltip from reappearing + updatePosition.cancel(); + clearHovering(); + }, + [], + ); + + // TODO: should this be extracted? + useEffect( + () => { + return () => { + // cancel the scheduled update of the container's dimension + if (animaFrameIDRef.current) { + window.cancelAnimationFrame(animaFrameIDRef.current); + } + }; + }, + [], + ); + + const detectionAreas = collisionComponents.map((area: JSX.Element, dataIndex: number) => { + const handleCurrentTooltip = handleTooltip(dataIndex); + return React.cloneElement(area, { + onTouchStart: handleCurrentTooltip, + onTouchMove: handleCurrentTooltip, + onMouseMove: handleCurrentTooltip, + onMouseLeave: hideTooltip, }); - - return ( - // Render areas to detect collisions of mouse pointers or touches with data points - detectionAreas - ); - } -} + }); + + // Render areas to detect collisions of mouse pointers or touches with data points + return ( + <> + {detectionAreas} + + ); +}; From f6e7cb3b9acb8e146d01f98665e854a99f09d66d Mon Sep 17 00:00:00 2001 From: garfieldduck Date: Tue, 26 Feb 2019 17:55:02 +0800 Subject: [PATCH 2/5] Extract the animation frame control as a hook --- packages/graph/src/hooks/useAnimationFrame.ts | 43 +++++++++++++++++++ .../graph/src/hooks/useContainerDimension.ts | 12 +++--- packages/graph/src/index.ts | 1 + packages/graph/src/layers/HoverLayer.tsx | 21 +++------ 4 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 packages/graph/src/hooks/useAnimationFrame.ts diff --git a/packages/graph/src/hooks/useAnimationFrame.ts b/packages/graph/src/hooks/useAnimationFrame.ts new file mode 100644 index 0000000..075a56a --- /dev/null +++ b/packages/graph/src/hooks/useAnimationFrame.ts @@ -0,0 +1,43 @@ +import { + useRef, + useEffect, + useCallback, +} from 'react'; + +export interface AnimationFrameControl { + /** The current animation frame ID */ + animationFrame: number | null; + + /** Function that takes `rafCallback` and put it into `window.requestAnimationFrame()` */ + requestWindowAnimationFrame: (rafCallback: () => void) => void; +} + +export function useAnimationFrame(): AnimationFrameControl { + /** stores the animation frame ID */ + const animaFrameIdRef = useRef(null); + + const requestWindowAnimationFrame = useCallback( + (rafCallback) => { + animaFrameIdRef.current = window.requestAnimationFrame(rafCallback); + }, + [], + ); + + useEffect( + () => { + // the functional component unmounting + return () => { + // cancel the scheduled update on the animation frame + if (animaFrameIdRef.current) { + window.cancelAnimationFrame(animaFrameIdRef.current); + } + }; + }, + [], + ); + + return { + requestWindowAnimationFrame, + animationFrame: animaFrameIdRef.current, + }; +} diff --git a/packages/graph/src/hooks/useContainerDimension.ts b/packages/graph/src/hooks/useContainerDimension.ts index acf4290..8b072dd 100644 --- a/packages/graph/src/hooks/useContainerDimension.ts +++ b/packages/graph/src/hooks/useContainerDimension.ts @@ -10,6 +10,8 @@ import resizeObserverPolyfill from 'resize-observer-polyfill'; import { GraphDimension } from '../common/types'; +import { useAnimationFrame } from './useAnimationFrame'; + interface ResizeObserverEntry { readonly target: Element; readonly contentRect: DOMRectReadOnly; @@ -26,13 +28,13 @@ export function useContainerDimension( /** resizeObsrRef.current stores the ResizeObserver */ const resizeObsrRef = useRef(null); - /** animaFrameIDRef.current stores the current animation frame ID */ - const animaFrameIDRef = useRef(null); + /** use requestAnimationFrame to update the dimension */ + const { requestWindowAnimationFrame } = useAnimationFrame(); /** Function to set the updated dimension */ const updateDimension = useCallback( (width: number, height: number) => { - animaFrameIDRef.current = window.requestAnimationFrame(() => { + requestWindowAnimationFrame(() => { setDimension({ width, height, @@ -69,10 +71,6 @@ export function useContainerDimension( resizeObsrRef.current.observe(containerRef.current!); return () => { - // cancel the scheduled update of the container's dimension - if (animaFrameIDRef.current) { - window.cancelAnimationFrame(animaFrameIDRef.current); - } // disconnect the resize observer on unmounted resizeObsrRef.current!.disconnect(); }; diff --git a/packages/graph/src/index.ts b/packages/graph/src/index.ts index 605bfe4..d7fee2d 100644 --- a/packages/graph/src/index.ts +++ b/packages/graph/src/index.ts @@ -1,6 +1,7 @@ export * from './components/Foo'; export * from './common/types'; export * from './hooks/useContainerDimension'; +export * from './hooks/useAnimationFrame'; export * from './layers/ResponsiveLayer'; export * from './layers/DataLayer'; export * from './layers/AxisLayer'; diff --git a/packages/graph/src/layers/HoverLayer.tsx b/packages/graph/src/layers/HoverLayer.tsx index 8b0038c..f0083fb 100644 --- a/packages/graph/src/layers/HoverLayer.tsx +++ b/packages/graph/src/layers/HoverLayer.tsx @@ -7,6 +7,8 @@ import React, { import { throttle } from 'lodash-es'; import { localPoint } from '@vx/event'; +import { useAnimationFrame } from '../hooks/useAnimationFrame'; + import { DataLayerRenderParams } from './DataLayer'; export interface HoverLayerProps { @@ -36,8 +38,8 @@ export const HoverLayer: FunctionComponent = ({ collisionComponents, throttleTime = 180, }) => { - /** stores the animation frame ID of the scheduled update of hovered position and data index */ - const animaFrameIDRef = useRef(null); + /** use requestAnimationFrame to schedule updates of hovered position and data index */ + const { requestWindowAnimationFrame } = useAnimationFrame(); /** Function to update the position of the tooltip and sets the currently active data index */ const updatePosition = useCallback( @@ -48,7 +50,7 @@ export const HoverLayer: FunctionComponent = ({ // convert the position of the event to the coordinate system of the SVG const { x, y } = localPoint(event); - animaFrameIDRef.current = window.requestAnimationFrame(() => { + requestWindowAnimationFrame(() => { setHoveredPosAndIndex( dataIndex, x, @@ -81,19 +83,6 @@ export const HoverLayer: FunctionComponent = ({ [], ); - // TODO: should this be extracted? - useEffect( - () => { - return () => { - // cancel the scheduled update of the container's dimension - if (animaFrameIDRef.current) { - window.cancelAnimationFrame(animaFrameIDRef.current); - } - }; - }, - [], - ); - const detectionAreas = collisionComponents.map((area: JSX.Element, dataIndex: number) => { const handleCurrentTooltip = handleTooltip(dataIndex); return React.cloneElement(area, { From 16933b1eef2761361048cf4ab7e070a4fbf0bfa4 Mon Sep 17 00:00:00 2001 From: garfieldduck Date: Tue, 26 Feb 2019 18:11:16 +0800 Subject: [PATCH 3/5] Update CHANGELOG for the rewrite of HoverLayer --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df6a427..12b228e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Extract the animation frame controls as a custom effect hook. (#10) - Add `tslint-react-hooks` rules to lint React Hooks. (#8) - Add `ThemeProvider` and color / xy axis themes config for customize theme. (#6) - Create an animation package, and add a simple SVG clipping animation: ``. (#4) @@ -22,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Makes simple components such as `` and `` as an experiment to see if the project settings go well. (#1) # Changed +- Rewrite `` using Hooks. (#10) - Use Hooks to rewrite functionalities of ``. (#8) - Refactor the way getting width and height in ``. (#8) - Fix `yarn lint` command. (#7) From eaa024975b56fdd101e7531856ddba2bf941dee8 Mon Sep 17 00:00:00 2001 From: garfieldduck Date: Mon, 4 Mar 2019 11:28:01 +0800 Subject: [PATCH 4/5] Fix useCallback by specifiying changeable inputs --- packages/graph/src/layers/HoverLayer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graph/src/layers/HoverLayer.tsx b/packages/graph/src/layers/HoverLayer.tsx index f0083fb..bac48fc 100644 --- a/packages/graph/src/layers/HoverLayer.tsx +++ b/packages/graph/src/layers/HoverLayer.tsx @@ -60,7 +60,7 @@ export const HoverLayer: FunctionComponent = ({ }, throttleTime, ), - [], + [handleHover, setHoveredPosAndIndex, throttleTime], ); /** Function to keep the event data and perform throttled updates of the position */ @@ -70,7 +70,7 @@ export const HoverLayer: FunctionComponent = ({ event.persist(); updatePosition(dataIndex, event); }, - [], + [updatePosition], ); /** Function to cancel the update of position and disable the hovering state */ @@ -80,7 +80,7 @@ export const HoverLayer: FunctionComponent = ({ updatePosition.cancel(); clearHovering(); }, - [], + [updatePosition, clearHovering], ); const detectionAreas = collisionComponents.map((area: JSX.Element, dataIndex: number) => { From 121db52f586284bcac84a19b2e6da42ad07c98aa Mon Sep 17 00:00:00 2001 From: garfieldduck Date: Mon, 4 Mar 2019 14:42:11 +0800 Subject: [PATCH 5/5] Remove the unused imports --- packages/graph/src/layers/HoverLayer.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/graph/src/layers/HoverLayer.tsx b/packages/graph/src/layers/HoverLayer.tsx index bac48fc..3d7ff5e 100644 --- a/packages/graph/src/layers/HoverLayer.tsx +++ b/packages/graph/src/layers/HoverLayer.tsx @@ -1,9 +1,4 @@ -import React, { - FunctionComponent, - useRef, - useEffect, - useCallback, -} from 'react'; +import React, { FunctionComponent, useCallback } from 'react'; import { throttle } from 'lodash-es'; import { localPoint } from '@vx/event';