Skip to content

Commit adf6c2d

Browse files
Merge pull request #174 from StackOverflowIsBetterThanAnyAI/171-refactor-outsource-useeffect
refactor: outsource useeffect
2 parents 1831a41 + 8f16c10 commit adf6c2d

26 files changed

+610
-456
lines changed

src/App.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
import {
2-
Dispatch,
3-
SetStateAction,
4-
createContext,
5-
useEffect,
6-
useState,
7-
} from 'react'
1+
import { Dispatch, SetStateAction, createContext, useState } from 'react'
82
import Footer from './components/UI/footer/Footer'
93
import StreamFeed from './components/streamFeed/StreamFeed'
104
import Navigation from './components/UI/navigation/Navigation'
115
import { StreamProps } from './types/StreamProps'
12-
import { getEnglishLanguageName } from './helper/getEnglishLanguageName'
136
import { getItemFromStorage } from './helper/getItemFromStorage'
7+
import { useDocumentTitle } from './hooks/useDocumentTitle'
148
import { useScreenWidth } from './hooks/useScreenWidth'
159

1610
export const ContextScreenWidth = createContext<
@@ -85,11 +79,7 @@ const App = () => {
8579
const [inputFocussed, setInputFocussed] = useState(false)
8680
const [hideSearch, setHideSearch] = useState(true)
8781

88-
useEffect(() => {
89-
document.title = `Twitch-App | ${getEnglishLanguageName(
90-
language
91-
)} Livestreams${seoSearchText ? ` | ${seoSearchText}` : ''}`
92-
}, [language, seoSearchText])
82+
useDocumentTitle(language, seoSearchText)
9383

9484
return (
9585
<div className="min-w-64 min-h-screen bg-zinc-800">

src/components/UI/navigation/DesktopSearch.tsx

Lines changed: 11 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { forwardRef, useContext, useEffect, useRef } from 'react'
1+
import { forwardRef, useContext, useRef } from 'react'
2+
import './search.css'
23
import {
34
ContextDisableFocusTrap,
45
ContextFocusInput,
56
ContextSearchResults,
67
ContextSearchText,
78
} from '../../../App'
8-
import { SearchProps } from '../../../types/SearchProps'
99
import Icon from '../Icon'
1010
import SearchResultSuggestion from './SearchResultSuggestion'
11-
import './search.css'
11+
import { SearchProps } from '../../../types/SearchProps'
12+
import { useFocusInput } from '../../../hooks/useFocusInput'
13+
import { useFocusTrapSearch } from '../../../hooks/useFocusTrapSearch'
1214

1315
const DesktopSearch = forwardRef<HTMLDivElement, SearchProps>(
1416
(
@@ -66,68 +68,14 @@ const DesktopSearch = forwardRef<HTMLDivElement, SearchProps>(
6668
const buttonRef = useRef<HTMLButtonElement | null>(null)
6769
const searchResultsRef = useRef<HTMLDivElement | null>(null)
6870

69-
useEffect(() => {
70-
const handleFocusTrap = (e: KeyboardEvent) => {
71-
if (
72-
searchText.length === 0 ||
73-
focusTrapDisabled ||
74-
e.key !== 'Tab'
75-
)
76-
return
77-
78-
const focusableElements = [
79-
inputRef?.current,
80-
buttonRef.current,
81-
...(searchResultsRef.current
82-
? Array.from(
83-
searchResultsRef.current.querySelectorAll(
84-
'button'
85-
)
86-
)
87-
: []),
88-
].filter((el) => el !== null) as (
89-
| HTMLInputElement
90-
| HTMLButtonElement
91-
)[]
92-
93-
if (focusableElements.length === 0) return
94-
95-
const firstElement = focusableElements[0]
96-
const lastElement =
97-
focusableElements[focusableElements.length - 1]
98-
99-
if (e.shiftKey) {
100-
if (document.activeElement === firstElement) {
101-
e.preventDefault()
102-
lastElement.focus()
103-
}
104-
} else {
105-
if (document.activeElement === lastElement) {
106-
e.preventDefault()
107-
firstElement.focus()
108-
}
109-
}
110-
}
111-
112-
document.addEventListener('keydown', handleFocusTrap)
113-
114-
return () => {
115-
document.removeEventListener('keydown', handleFocusTrap)
116-
}
117-
}, [
71+
useFocusTrapSearch(
72+
buttonRef,
11873
focusTrapDisabled,
11974
inputRef,
120-
searchResults,
121-
searchResultsExpanded,
122-
searchText,
123-
])
124-
125-
useEffect(() => {
126-
if (inputFocussed && inputRef?.current) {
127-
inputRef.current.focus()
128-
setInputFocussed(false)
129-
}
130-
}, [inputFocussed, inputRef, setInputFocussed])
75+
searchResultsRef,
76+
searchText
77+
)
78+
useFocusInput(inputFocussed, inputRef, setInputFocussed)
13179

13280
return (
13381
<div

src/components/UI/navigation/MobileSearch.tsx

Lines changed: 11 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import { forwardRef, useContext, useEffect, useRef } from 'react'
1+
import { forwardRef, useContext, useRef } from 'react'
2+
import './search.css'
23
import {
34
ContextDisableFocusTrap,
45
ContextFocusInput,
56
ContextSearchResults,
67
ContextSearchText,
78
} from '../../../App'
8-
import { SearchProps } from '../../../types/SearchProps'
99
import Icon from '../Icon'
1010
import SearchResultSuggestion from './SearchResultSuggestion'
11-
import './search.css'
11+
import { SearchProps } from '../../../types/SearchProps'
12+
import { useFocusInput } from '../../../hooks/useFocusInput'
13+
import { useFocusTrapSearch } from '../../../hooks/useFocusTrapSearch'
1214

1315
const MobileSearch = forwardRef<HTMLDivElement, SearchProps>(
1416
(
@@ -66,62 +68,14 @@ const MobileSearch = forwardRef<HTMLDivElement, SearchProps>(
6668
const buttonRef = useRef<HTMLButtonElement | null>(null)
6769
const searchResultsRef = useRef<HTMLDivElement | null>(null)
6870

69-
useEffect(() => {
70-
const handleFocusTrap = (e: KeyboardEvent) => {
71-
if (e.key !== 'Tab' || focusTrapDisabled) return
72-
73-
const focusableElements = [
74-
searchMobileRef?.current,
75-
buttonRef.current,
76-
...(searchResultsRef.current
77-
? Array.from(
78-
searchResultsRef.current.querySelectorAll(
79-
'button'
80-
)
81-
)
82-
: []),
83-
].filter((el) => el !== null) as (
84-
| HTMLInputElement
85-
| HTMLButtonElement
86-
)[]
87-
88-
if (focusableElements.length === 0) return
89-
90-
const firstElement = focusableElements[0]
91-
const lastElement =
92-
focusableElements[focusableElements.length - 1]
93-
94-
if (e.shiftKey) {
95-
if (document.activeElement === firstElement) {
96-
e.preventDefault()
97-
lastElement.focus()
98-
}
99-
} else {
100-
if (document.activeElement === lastElement) {
101-
e.preventDefault()
102-
firstElement.focus()
103-
}
104-
}
105-
}
106-
107-
document.addEventListener('keydown', handleFocusTrap)
108-
109-
return () => {
110-
document.removeEventListener('keydown', handleFocusTrap)
111-
}
112-
}, [
71+
useFocusTrapSearch(
72+
buttonRef,
11373
focusTrapDisabled,
11474
searchMobileRef,
115-
searchResults,
116-
searchResultsExpanded,
117-
])
118-
119-
useEffect(() => {
120-
if (inputFocussed && searchMobileRef?.current) {
121-
searchMobileRef.current.focus()
122-
setInputFocussed(false)
123-
}
124-
}, [inputFocussed, searchMobileRef, setInputFocussed])
75+
searchResultsRef,
76+
searchText
77+
)
78+
useFocusInput(inputFocussed, searchMobileRef, setInputFocussed)
12579

12680
return (
12781
<>

src/components/UI/navigation/Navigation.tsx

Lines changed: 24 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { useContext, useEffect, useRef, useState } from 'react'
1+
import { useContext, useRef, useState } from 'react'
22
import ButtonIcon from '../ButtonIcon'
3+
import DesktopSearch from './DesktopSearch'
4+
import MobileSearch from './MobileSearch'
35
import { HomeIcon } from './HomeIcon'
46
import { UserIcon } from './UserIcon'
5-
import MobileSearch from './MobileSearch'
67
import {
78
ContextDisableFocusTrap,
89
ContextFilteredStreamData,
@@ -14,9 +15,11 @@ import {
1415
ContextSearchText,
1516
ContextStreamData,
1617
} from '../../../App'
17-
import DesktopSearch from './DesktopSearch'
1818
import { getSearchFilter } from '../../../helper/getSearchFilter'
1919
import { setItemInStorage } from '../../../helper/setItemInStorage'
20+
import { useNavigationScrollY } from '../../../hooks/useNavigationScrollY'
21+
import { useCloseSearchResults } from '../../../hooks/useCloseSearchResults'
22+
import { useHideMobileSearch } from '../../../hooks/useHideMobileSearch'
2023

2124
const Navigation = () => {
2225
const contextScreenWidth = useContext(ContextScreenWidth)
@@ -253,103 +256,24 @@ const Navigation = () => {
253256
}, 0)
254257
}
255258

256-
useEffect(() => {
257-
let lastScrollY = window.scrollY
258-
let timer: NodeJS.Timeout
259-
260-
const handleScroll = () => {
261-
if (window.scrollY === 0) {
262-
setNavOpacity('opacity-100')
263-
} else if (window.scrollY < lastScrollY && !blockOpacity) {
264-
setNavOpacity('opacity-75')
265-
} else if (!blockOpacity) {
266-
setNavOpacity('opacity-95')
267-
}
268-
lastScrollY = window.scrollY
269-
270-
clearTimeout(timer)
271-
if (navOpacity !== 'opacity-100') {
272-
timer = setTimeout(() => {
273-
setNavOpacity('opacity-100')
274-
}, 500)
275-
}
276-
}
277-
278-
window.addEventListener('scroll', handleScroll)
279-
280-
return () => {
281-
window.removeEventListener('scroll', handleScroll)
282-
clearTimeout(timer)
283-
}
284-
}, [blockOpacity, navOpacity])
285-
286-
useEffect(() => {
287-
const handleClickOutside = (event: MouseEvent) => {
288-
if (
289-
((desktopSearchRef.current &&
290-
!desktopSearchRef.current.contains(event.target as Node)) ||
291-
(mobileSearchRef.current &&
292-
!mobileSearchRef.current.contains(
293-
event.target as Node
294-
))) &&
295-
searchResultsExpanded
296-
) {
297-
event.stopPropagation()
298-
setSearchResultsExpanded(false)
299-
}
300-
}
301-
302-
const handleEscape = (event: KeyboardEvent) => {
303-
if (
304-
((desktopSearchRef.current &&
305-
desktopSearchRef.current.contains(event.target as Node)) ||
306-
(mobileSearchRef.current &&
307-
mobileSearchRef.current.contains(
308-
event.target as Node
309-
))) &&
310-
searchResultsExpanded &&
311-
event.key === 'Escape'
312-
) {
313-
event.stopPropagation()
314-
setSearchResultsExpanded(false)
315-
if (!inputRef?.current?.onfocus) {
316-
if (
317-
contextScreenWidth === 'MOBILE' ||
318-
contextScreenWidth === 'TABLET_SMALL'
319-
) {
320-
buttonIconRef?.current?.focus()
321-
} else {
322-
userIconRef?.current?.focus()
323-
anchorRef?.current?.focus()
324-
}
325-
}
326-
}
327-
}
328-
329-
if (searchResultsExpanded) {
330-
document.addEventListener('keydown', handleEscape)
331-
document.addEventListener('mousedown', handleClickOutside)
332-
} else {
333-
document.removeEventListener('keydown', handleEscape)
334-
document.removeEventListener('mousedown', handleClickOutside)
335-
}
336-
337-
return () => {
338-
document.removeEventListener('keydown', handleEscape)
339-
document.removeEventListener('mousedown', handleClickOutside)
340-
}
341-
}, [contextScreenWidth, searchResultsExpanded])
342-
343-
useEffect(() => {
344-
if (
345-
(contextScreenWidth === 'MOBILE' ||
346-
contextScreenWidth === 'TABLET_SMALL') &&
347-
inputFocussed
348-
) {
349-
setHideSearch(false)
350-
setAriaPressed(true)
351-
}
352-
}, [contextScreenWidth, inputFocussed, setHideSearch])
259+
useNavigationScrollY(blockOpacity, navOpacity, setNavOpacity)
260+
useCloseSearchResults(
261+
anchorRef,
262+
buttonIconRef,
263+
contextScreenWidth,
264+
desktopSearchRef,
265+
inputRef,
266+
mobileSearchRef,
267+
searchResultsExpanded,
268+
setSearchResultsExpanded,
269+
userIconRef
270+
)
271+
useHideMobileSearch(
272+
contextScreenWidth,
273+
inputFocussed,
274+
setAriaPressed,
275+
setHideSearch
276+
)
353277

354278
return (
355279
<div className="sticky top-0 z-10" data-testid="navigation-container">

0 commit comments

Comments
 (0)