-
Notifications
You must be signed in to change notification settings - Fork 141
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
chore: hooksのロジックを整理する #5361
base: master
Are you sure you want to change the base?
chore: hooksのロジックを整理する #5361
Changes from 15 commits
81ee30b
a538169
8b74670
a21e23c
dd08b36
e4be2b9
5aa3fe3
6d187cf
a9d74b7
df24151
b7c1e8d
d21c0a2
60684a4
5a177c1
1ac66a5
757956c
aa9b87c
4a77d14
28577b1
4f36150
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 |
---|---|---|
@@ -1,34 +1,35 @@ | ||
import { RefObject, useCallback, useEffect } from 'react' | ||
import { RefObject, useEffect } from 'react' | ||
|
||
export function useClick( | ||
innerRefs: Array<RefObject<HTMLElement>>, | ||
innerCallback: (e: MouseEvent) => void, | ||
outerCallback: (e: MouseEvent) => void, | ||
) { | ||
const handleClick = useCallback( | ||
(e: MouseEvent) => { | ||
useEffect(() => { | ||
const handleClick = (e: MouseEvent) => { | ||
if (innerRefs.some((target) => isEventIncludedParent(e, target.current))) { | ||
innerCallback(e) | ||
|
||
return | ||
} | ||
|
||
outerCallback(e) | ||
}, | ||
// spread innerRefs to compare deps one by one | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[...innerRefs, innerCallback, outerCallback], | ||
) | ||
} | ||
|
||
useEffect(() => { | ||
window.addEventListener('click', handleClick) | ||
|
||
return () => { | ||
window.removeEventListener('click', handleClick) | ||
} | ||
}, [handleClick]) | ||
}, [innerRefs, innerCallback, outerCallback]) | ||
} | ||
|
||
function isEventIncludedParent(e: MouseEvent, parent: Element | null): boolean { | ||
if (!parent) return false | ||
|
||
const path = e.composedPath() | ||
if (path.length === 0 || !parent) return false | ||
|
||
if (path.length === 0) return false | ||
Comment on lines
+28
to
+32
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. pathの生成が不要なパターンに対するチェックを最適化しています |
||
|
||
return path.includes(parent) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,19 @@ | ||
import { useCallback, useEffect } from 'react' | ||
import { useEffect } from 'react' | ||
|
||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key | ||
// Esc is a IE/Edge specific value | ||
const ESCAPE_KEY_REGEX = /^Esc(ape)?$/ | ||
|
||
export const useHandleEscape = (cb: () => void) => { | ||
const handleKeyPress = useCallback( | ||
(e: KeyboardEvent) => { | ||
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key | ||
// Esc is a IE/Edge specific value | ||
if (e.key === 'Escape' || e.key === 'Esc') { | ||
useEffect(() => { | ||
const handleKeyPress = (e: KeyboardEvent) => { | ||
if (ESCAPE_KEY_REGEX.test(e.key)) { | ||
cb() | ||
} | ||
}, | ||
[cb], | ||
) | ||
useEffect(() => { | ||
} | ||
|
||
document.addEventListener('keydown', handleKeyPress) | ||
|
||
return () => document.removeEventListener('keydown', handleKeyPress) | ||
}, [handleKeyPress]) | ||
}, [cb]) | ||
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. handleKeyPressはuseEffect中でしか利用されていないため、ひとまとめにしています |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,30 @@ | ||
import { RefObject, useCallback, useEffect } from 'react' | ||
import { RefObject, useEffect } from 'react' | ||
|
||
export function useOuterClick( | ||
targets: Array<RefObject<HTMLElement>>, | ||
callback: (e: MouseEvent) => void, | ||
) { | ||
const handleOuterClick = useCallback( | ||
(e: MouseEvent) => { | ||
if (targets.some((target) => isEventIncludedParent(e, target.current))) { | ||
return | ||
useEffect(() => { | ||
const handleOuterClick = (e: MouseEvent) => { | ||
if (targets.every((target) => isEventExcludedParent(e, target.current))) { | ||
callback(e) | ||
} | ||
callback(e) | ||
}, | ||
// spread targets to compare deps one by one | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[...targets, callback], | ||
) | ||
} | ||
Comment on lines
-7
to
+12
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. 早期returnが直後の処理の逆転条件でしかないため、理解がワンテンポ遅れることになり、デメリットしかない状態になっています。 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. 理解がワンテンポ遅れるのは個人の感覚かなと思います。 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.
すべての早期returnがそう、という話ではなく、今回の場合は以下の様になっていたためです。
上記コードは本質的には
こっちのほうが素直では?という話です。 |
||
|
||
useEffect(() => { | ||
window.addEventListener('click', handleOuterClick) | ||
|
||
return () => { | ||
window.removeEventListener('click', handleOuterClick) | ||
} | ||
}, [handleOuterClick]) | ||
}, [callback, targets]) | ||
} | ||
|
||
function isEventIncludedParent(e: MouseEvent, parent: Element | null): boolean { | ||
function isEventExcludedParent(e: MouseEvent, parent: Element | null): boolean { | ||
if (!parent) return false | ||
|
||
const path = e.composedPath() | ||
if (path.length === 0 || !parent) return false | ||
return path.includes(parent) | ||
|
||
if (path.length === 0) return false | ||
|
||
return !path.includes(parent) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,15 @@ export function usePortal() { | |
const [portalRoot, setPortalRoot] = useState<HTMLDivElement | null>(null) | ||
const currentSeq = useMemo(() => ++portalSeq, []) | ||
const parent = useContext(ParentContext) | ||
const parentSeqs = parent.seqs.concat(currentSeq) | ||
|
||
const calculatedSeqs = useMemo(() => { | ||
const parentSeqs = parent.seqs.concat(currentSeq) | ||
|
||
return { | ||
parentSeqs, | ||
portalChildOf: parentSeqs.join(','), | ||
} | ||
}, [currentSeq, parent.seqs]) | ||
Comment on lines
+29
to
+36
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. このファイルがやっていることはややこしいのですが、ざっくりいうと以下のとおりです
このチェックのため、data属性を利用していますが、data属性は基本stringしか突っ込めないので、joinでつなげるようにしています |
||
|
||
useEnhancedEffect(() => { | ||
// Next.jsのhydration error回避のため、初回レンダリング時にdivを作成する | ||
|
@@ -36,36 +44,37 @@ export function usePortal() { | |
if (!portalRoot) { | ||
return | ||
} | ||
portalRoot.dataset.portalChildOf = parentSeqs.join(',') | ||
|
||
portalRoot.dataset.portalChildOf = calculatedSeqs.portalChildOf | ||
document.body.appendChild(portalRoot) | ||
|
||
return () => { | ||
document.body.removeChild(portalRoot) | ||
} | ||
// spread parentSeqs array for deps | ||
}, [portalRoot, ...parentSeqs]) | ||
}, [portalRoot, calculatedSeqs.portalChildOf]) | ||
|
||
const isChildPortal = useCallback( | ||
(element: HTMLElement | null) => _isChildPortal(element, currentSeq), | ||
(element: HTMLElement | null) => _isChildPortal(element, new RegExp(`(^|,)${currentSeq}(,|$)`)), | ||
[currentSeq], | ||
) | ||
|
||
const PortalParentProvider: FC<{ children: ReactNode }> = useCallback( | ||
({ children }) => { | ||
const value: ParentContextValue = { | ||
seqs: parentSeqs, | ||
seqs: calculatedSeqs.parentSeqs, | ||
} | ||
|
||
return <ParentContext.Provider value={value}>{children}</ParentContext.Provider> | ||
}, | ||
// spread parentSeqs array for deps | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
[...parentSeqs], | ||
[calculatedSeqs.parentSeqs], | ||
) | ||
|
||
const wrappedCreatePortal = useCallback( | ||
(children: ReactNode) => { | ||
if (portalRoot === null) { | ||
return null | ||
} | ||
|
||
return createPortal(children, portalRoot) | ||
}, | ||
[portalRoot], | ||
|
@@ -82,12 +91,15 @@ export function usePortal() { | |
} | ||
} | ||
|
||
function _isChildPortal( | ||
element: HTMLElement | SVGElement | null, | ||
parentPortalSeq: number, | ||
): boolean { | ||
function _isChildPortal(element: HTMLElement | SVGElement | null, seqRegex: RegExp): boolean { | ||
if (!element) return false | ||
const childOf = element.dataset?.portalChildOf || '' | ||
const includesSeq = childOf.split(',').includes(String(parentPortalSeq)) | ||
return includesSeq || _isChildPortal(element.parentElement, parentPortalSeq) | ||
|
||
let includesSeq = false | ||
const childOf = element.dataset?.portalChildOf | ||
|
||
if (childOf) { | ||
includesSeq = seqRegex.test(childOf) | ||
} | ||
Comment on lines
-90
to
+102
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. これまでのロジックでは |
||
|
||
return includesSeq || _isChildPortal(element.parentElement, seqRegex) | ||
} |
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.
handleClickはuseEffectの中でしか利用されていないため、ひとまとめにしています。
依存関係の配列も展開は微妙なのでmemo化したものが渡されてくるように修正しています