Skip to content

Commit

Permalink
refactor: reorganize contexts to avoid menu rerendering when start fi…
Browse files Browse the repository at this point in the history
…ltering
  • Loading branch information
isqua committed Oct 25, 2023
1 parent 53d205a commit 41df088
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 70 deletions.
22 changes: 17 additions & 5 deletions src/features/toc/ui/Menu/Context/MenuProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react'
import { useCallback, useMemo, type PropsWithChildren } from 'react'
import { useFilter } from '../../../../../hooks/useFilter'
import { filterTreeNodes } from '../../../core/filterTreeNodes'
import { getBreadCrumbs } from '../../../core/getBreadCrumbs'
Expand All @@ -12,10 +12,22 @@ type MenuProviderProps = PropsWithChildren<{
}>

export function MenuProvider({ toc, url, children, isLoading = false }: MenuProviderProps): JSX.Element {
const breadcrumbs = getBreadCrumbs(toc, url)
const tocContextValue = { toc, isLoading }
const locationContextValue = { url, breadcrumbs }
const filterContextValue = useFilter((text) => filterTreeNodes(toc, text))
const filterCallback = useCallback(
(text: string) => filterTreeNodes(toc, text),
[toc]
)

const { data, manager: filterContextValue } = useFilter(filterCallback)

const tocContextValue = useMemo(
() => ({ toc, filter: data, isLoading }),
[toc, data, isLoading],
)

const locationContextValue = useMemo(
() => ({ url, breadcrumbs: getBreadCrumbs(toc, url) }),
[toc, url]
)

return (
<TocContext.Provider value={tocContextValue}>
Expand Down
20 changes: 16 additions & 4 deletions src/features/toc/ui/Menu/Context/contexts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createContext } from 'react'

import { initialFilterState, noopFilterActions, type UseFilterResult } from '../../../../../hooks/useFilter'
import { FilterActions, noopFilterActions } from '../../../../../hooks/useFilter'
import type { PageDescriptor, PageURL, TableOfContent } from '../../../types'

type TocContextValue = {
toc: TableOfContent
filter: Set<PageDescriptor> | null
isLoading: boolean
}

Expand All @@ -13,6 +14,10 @@ type LocationContextValue = {
breadcrumbs: PageDescriptor[]
}

type FilterContextValue = FilterActions & {
isFiltering: boolean
}

const defaultToc: TableOfContent = {
topLevelIds: [],
entities: { pages: {} },
Expand All @@ -23,15 +28,22 @@ const defaultBreadCrumbs: PageDescriptor[] = []

export const TocContext = createContext<TocContextValue>({
toc: defaultToc,
filter: null,
isLoading: true,
})

TocContext.displayName = 'TocContext'

export const LocationContext = createContext<LocationContextValue>({
url: defaultUrl,
breadcrumbs: defaultBreadCrumbs,
})

export const FilterContext = createContext<UseFilterResult<Set<PageDescriptor>>>({
state: initialFilterState,
actions: noopFilterActions,
LocationContext.displayName = 'LocationContext'

export const FilterContext = createContext<FilterContextValue>({
isFiltering: false,
...noopFilterActions
})

FilterContext.displayName = 'FilterContext'
18 changes: 5 additions & 13 deletions src/features/toc/ui/Menu/Context/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,11 @@ import { buildMenuSection } from '../../../core/buildMenuSection'
import type { PageId, SectionHighlight } from '../../../types'
import { FilterContext, LocationContext, TocContext } from './contexts'

const useFilterData = () => {
const { state: { data } } = useContext(FilterContext)

return data
}

const FILTER_DELAY_IN_MS = 1000

export const useSectionItems = (parentId: PageId = '', level: number = 0, highlight: SectionHighlight) => {
const { toc } = useContext(TocContext)
const { toc, filter } = useContext(TocContext)
const currentLocation = useContext(LocationContext)
const filter = useFilterData()

const items = buildMenuSection(toc, {
url: currentLocation.url,
Expand All @@ -36,8 +29,7 @@ export const useIsLoading = () => {
}

export const useFilterInput = () => {
const { actions, state: { isLoading } } = useContext(FilterContext)
const { onChange, onFilterStart, onReset } = actions
const { isFiltering, onChange, onFilterStart, onReset } = useContext(FilterContext)
const timeout = useRef<number>(0)

const onChangeHandler = useCallback((value: string) => {
Expand All @@ -48,7 +40,7 @@ export const useFilterInput = () => {
return
}

if (!isLoading) {
if (!isFiltering) {
onFilterStart()
}

Expand All @@ -59,11 +51,11 @@ export const useFilterInput = () => {
timeout.current = window.setTimeout(() => {
onChange(text)
}, FILTER_DELAY_IN_MS)
}, [isLoading, onChange, onFilterStart, onReset])
}, [isFiltering, onChange, onFilterStart, onReset])

useEffect(() => () => {
clearTimeout(timeout.current)
}, [])

return { onChange: onChangeHandler, isLoading }
return { onChange: onChangeHandler, isFiltering }
}
4 changes: 2 additions & 2 deletions src/features/toc/ui/Menu/Filter/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { useFilterInput } from '../Context/hooks'
import styles from './Filter.module.css'

export function Filter(): JSX.Element {
const { onChange, isLoading } = useFilterInput()
const { onChange, isFiltering } = useFilterInput()

return (
<div className={styles.filter}>
<Input
onChange={onChange}
isLoading={isLoading}
isLoading={isFiltering}
placeholder='Filter menu'
/>
</div>
Expand Down
34 changes: 11 additions & 23 deletions src/hooks/useFilter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,28 @@ describe('hooks/useFilter', () => {

const { result } = renderHook(() => useFilter(filter))

expect(result.current.state).toEqual({
isLoading: false,
text: '',
data: null,
})
expect(result.current.data).toEqual(null)
expect(result.current.manager.isFiltering).toEqual(false)

act(() => {
result.current.actions.onFilterStart()
result.current.manager.onFilterStart()
})

expect(result.current.state).toEqual({
isLoading: true,
text: '',
data: null,
})
expect(result.current.data).toEqual(null)
expect(result.current.manager.isFiltering).toEqual(true)

act(() => {
result.current.actions.onChange('hello')
result.current.manager.onChange('hello')
})

expect(result.current.state).toEqual({
isLoading: false,
text: 'hello',
data: 'text: hello',
})
expect(result.current.data).toEqual('text: hello')
expect(result.current.manager.isFiltering).toEqual(false)

act(() => {
result.current.actions.onReset()
result.current.manager.onReset()
})

expect(result.current.state).toEqual({
isLoading: false,
text: '',
data: null,
})
expect(result.current.data).toEqual(null)
expect(result.current.manager.isFiltering).toEqual(false)
})
})
43 changes: 20 additions & 23 deletions src/hooks/useFilter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useReducer, type Reducer } from 'react'
import { useReducer, type Reducer, useMemo } from 'react'

interface DataFilter<T> {
(text: string): T
Expand All @@ -11,11 +11,11 @@ export type FilterActions = {
}

export type UseFilterResult<T> = {
state: FilterState<T>
actions: FilterActions
data: null | T
manager: FilterActions & { isFiltering: boolean }
}

type FilterState<T> = {
export type FilterState<T> = {
isLoading: boolean
text: string
data: null | T
Expand Down Expand Up @@ -75,26 +75,23 @@ export function useFilter<T>(fn: DataFilter<T>): UseFilterResult<T> {
initialFilterState,
)

const onFilterStart = () => {
dispatch({ type: 'start' })
}

const onChange = (text: string) => {
const data = fn(text)

dispatch({ type: 'change', text, data })
}

const onReset = () => {
dispatch({ type: 'reset' })
}
const manager = useMemo(() => ({
isFiltering: state.isLoading,
onFilterStart: () => {
dispatch({ type: 'start' })
},
onChange: (text: string) => {
const data = fn(text)

dispatch({ type: 'change', text, data })
},
onReset: () => {
dispatch({ type: 'reset' })
},
}), [fn, state.isLoading])

return {
state,
actions: {
onFilterStart,
onChange,
onReset,
}
data: state.data,
manager,
}
}

0 comments on commit 41df088

Please sign in to comment.