-
Notifications
You must be signed in to change notification settings - Fork 836
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create ZoomHandler #1354
base: master
Are you sure you want to change the base?
Create ZoomHandler #1354
Changes from 2 commits
1ee7dee
61a3c9d
e4bb21c
e51407b
c7332a0
10da92e
2932243
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import React, {useEffect, useState, useCallback, useRef} from 'react'; | ||
import {getCombinedClassName} from '../utils/styling-utils'; | ||
import {getAttributeScale} from '../utils/scales-utils'; | ||
|
||
const DEFAULT_STATE = { | ||
brushing: false, | ||
bounds: null, | ||
startPosition: null | ||
}; | ||
|
||
const useStateWithGet = initialState => { | ||
const [state, setState] = useState(initialState); | ||
const ref = useRef(); | ||
ref.current = state; | ||
const get = useCallback(() => ref.current, []); | ||
return [state, setState, get]; | ||
}; | ||
|
||
export default function ZoomHandler(props) { | ||
const { | ||
events: {mouseMove, mouseDown, mouseUp, mouseLeave}, | ||
onZoom, | ||
enableX = true, | ||
enableY = true, | ||
marginLeft = 0, | ||
marginTop = 0, | ||
innerWidth = 0, | ||
innerHeight = 0 | ||
} = props; | ||
|
||
const [state, setState, getState] = useStateWithGet(DEFAULT_STATE); | ||
|
||
const convertArea = useCallback( | ||
area => { | ||
const xScale = getAttributeScale(props, 'x'); | ||
const yScale = getAttributeScale(props, 'y'); | ||
|
||
return { | ||
bottom: yScale.invert(area.bottom), | ||
left: xScale.invert(area.left - marginLeft), | ||
right: xScale.invert(area.right - marginLeft), | ||
top: yScale.invert(area.top) | ||
}; | ||
}, | ||
[marginLeft, props] | ||
); | ||
|
||
const onMouseMove = useCallback( | ||
e => { | ||
const state = getState(); | ||
if (!state.brushing) { | ||
return; | ||
} | ||
e.stopPropagation(); | ||
e.preventDefault(); | ||
const position = getPosition(e); | ||
|
||
setState(state => { | ||
const bounds = { | ||
left: enableX | ||
? Math.min(position.x, state.startPosition.x) | ||
: marginLeft, | ||
top: enableY | ||
? Math.min(position.y, state.startPosition.y) | ||
: marginTop, | ||
right: enableX | ||
? Math.max(position.x, state.startPosition.x) | ||
: innerWidth + marginLeft, | ||
bottom: enableY | ||
? Math.max(position.y, state.startPosition.y) | ||
: innerHeight + marginTop | ||
}; | ||
return { | ||
...state, | ||
bounds | ||
}; | ||
}); | ||
}, | ||
[ | ||
enableX, | ||
enableY, | ||
getState, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The biggest culprit is the fact that I'm also wary about splitting up the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed I did not have in mind the need for That's too bad I thought splitting Since the goal is to improve perf, splitting the state wouldn't be an improvement so let's drop that suggestion |
||
innerHeight, | ||
innerWidth, | ||
marginLeft, | ||
marginTop, | ||
setState | ||
] | ||
); | ||
|
||
const onMouseDown = useCallback( | ||
e => { | ||
e.stopPropagation(); | ||
e.preventDefault(); | ||
const {x, y} = getPosition(e); | ||
|
||
const bounds = {left: x, top: y, right: x, bottom: y}; | ||
|
||
setState(state => ({ | ||
...state, | ||
brushing: true, | ||
bounds, | ||
startPosition: {x, y} | ||
})); | ||
}, | ||
[setState] | ||
); | ||
|
||
const onMouseUp = useCallback( | ||
e => { | ||
const state = getState(); | ||
|
||
if (!state.brushing) { | ||
return setState(DEFAULT_STATE); | ||
} | ||
|
||
e.stopPropagation(); | ||
e.preventDefault(); | ||
|
||
if ( | ||
state.bounds.bottom - state.bounds.top > 5 && | ||
state.bounds.right - state.bounds.left > 5 | ||
) { | ||
onZoom && onZoom(convertArea(state.bounds)); | ||
} | ||
|
||
setState(DEFAULT_STATE); | ||
}, | ||
[convertArea, getState, onZoom, setState] | ||
); | ||
|
||
const onMouseLeave = useCallback(() => { | ||
const state = getState(); | ||
if (state.brushing) { | ||
setState(DEFAULT_STATE); | ||
} | ||
}, [getState, setState]); | ||
|
||
useEffect(() => mouseMove.subscribe(onMouseMove), [mouseMove, onMouseMove]); | ||
useEffect(() => mouseDown.subscribe(onMouseDown), [mouseDown, onMouseDown]); | ||
useEffect(() => mouseUp.subscribe(onMouseUp), [mouseUp, onMouseUp]); | ||
useEffect(() => mouseLeave.subscribe(onMouseLeave), [ | ||
mouseLeave, | ||
onMouseLeave | ||
]); | ||
|
||
if (!state.brushing) { | ||
return null; | ||
} | ||
|
||
const {bounds} = state; | ||
const {opacity, color, className} = props; | ||
|
||
return ( | ||
<g className={getCombinedClassName(className, 'rv-highlight-container')}> | ||
<rect | ||
className="rv-highlight" | ||
pointerEvents="none" | ||
opacity={opacity} | ||
fill={color} | ||
x={bounds.left} | ||
y={bounds.top} | ||
width={Math.max(0, bounds.right - bounds.left)} | ||
height={Math.max(0, bounds.bottom - bounds.top)} | ||
/> | ||
</g> | ||
); | ||
} | ||
ZoomHandler.requiresSVG = true; | ||
|
||
function getPosition(evt) { | ||
const x = evt.nativeEvent.offsetX ?? evt.nativeEvent.pageX; | ||
const y = evt.nativeEvent.offsetY ?? evt.nativeEvent.pageY; | ||
return {x, y}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
export class Event { | ||
subscribers = []; | ||
|
||
constructor(name) { | ||
this.name = name; | ||
} | ||
|
||
fire(...args) { | ||
this.subscribers.forEach(cb => cb(...args)); | ||
} | ||
|
||
subscribe(callback) { | ||
this.subscribers.push(callback); | ||
return () => this.unsubscribe(callback); | ||
} | ||
|
||
unsubscribe(callback) { | ||
this.subscribers = this.subscribers.filter(x => x !== callback); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will move this to a shared location, however I left it here for now to give it some better visibility.
This acts just like
useState
however it returns a third parameter which is a function that returns the value.The function is memoized so there will only be one instance per component, however when called, it will return the current state.
This makes it ideal when the handlers, such as
useEffect
anduseCallback
need the current value, however it doesn't change the identity of handler.Using the
getState
in theuseCallback
s below drastically reduces the number of subscribe/unsubscribe calls, as well as the number of re-renders. Without this, the callback foronMouseUp
would be invalidated on every mouse move (while dragging)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not very comfortable with this, if I understand correctly it looks like a hack to get around the dependencies of
useCallback
anduseEffect
and this could lead to a bug.I am not talking about this specific case because I am not sure, but it could lead to situations where the state value is changed, but since
getState
is used as dependency for auseEffect
, it won't be called with the new valueThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, this is an approach to get around the dependencies and it could be abused which may cause bugs.
The main bug that I can see arising only really applies to
useMemo
.If the
getState
is passed into the dependencies ofuseMemo
a change ofstate
will not cause theuseMemo
handler to re-fire, and therefore the result would be based off of the previous value.With
useCallback
anduseEffect
, you won't really have these problems since they would always use the latest values, and their execution is not dependant on the render function.One other way to accomplish this would be
This example here, I find is much worse as the function passed to
setState
is no longer pure.Here is an issue discussing other ways around it as well. facebook/react#14543
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another, more standard way of doing this, would be as follows:
In the end this is the same as the custom hook, however this is more explicit.
And as such, it could be commented in-line to better explain why the functionality is like that.
This would also eliminate the temptation to use this in places where it would cause bugs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I follow. If I have a
useEffect
declaringgetState
as a dependency, I expectuseEffect
won't be re-run even if the state has changed.What do you mean by "execution not dependent on the render function" ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I agree, that was the case I meant in my first comment, and why I am not fond of giving a tool like this helper which can bring bugs but that's just my opinion. What K like about hooks is that if you follow the eslint rules and stick to them you should can this kind of bugs. In the meantime, I hear that performance issues can arise with too many re-renders
Yes again I agree this applies to
useCallback
but I was more on the fence for this statement aboutuseEffect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this helper brings a lot of value I won't fight over it even if I have concerns
I really like what you suggested in this comment: #1354 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya. I misspoke putting
useEffect
in that sentence. The point that I was trying to make with putting in there was that the value in the state doesn't necessarily affect the lifecycle of the effect.How do you feel about the explicit approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That way it is more self-explanatory and as you said we won't be tempted to use it in places it can cause bugs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I'll change it to use the explicit version.
I'll also make sure to comment it so that the reader knows why the state is being stored in the
ref