Ott 464 infinite scroll in tournaments filter (#166)

* fix(#464): lazy ref problem

* refactor(#464): used InfiniteScroll component in tournaments filter list

* Update src/features/InfiniteScroll/index.tsx

Co-authored-by: Andrey Razdorskiy <quitesocial@yandex.ru>

Co-authored-by: Andrey Razdorskiy <quitesocial@yandex.ru>
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent d767f266f2
commit a5e2aaabd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      src/features/HeaderFilters/components/TournamentFilter/hooks.tsx
  2. 8
      src/features/HeaderFilters/components/TournamentFilter/index.tsx
  3. 3
      src/features/HeaderFilters/components/TournamentFilter/styled.tsx
  4. 21
      src/features/HeaderFilters/components/TournamentList/index.tsx
  5. 13
      src/features/HeaderFilters/store/hooks/index.tsx
  6. 10
      src/features/InfiniteScroll/hooks.tsx
  7. 13
      src/features/InfiniteScroll/index.tsx
  8. 2
      src/features/Matches/index.tsx
  9. 10
      src/requests/getSportTournaments.tsx

@ -1,10 +1,11 @@
import type { BaseSyntheticEvent, MouseEvent } from 'react' import type { MouseEvent } from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import find from 'lodash/find' import find from 'lodash/find'
import type { Tournaments } from 'requests' import type { Tournaments } from 'requests'
import { getSportTournaments } from 'requests' import { getSportTournaments } from 'requests'
import { useToggle } from 'hooks' import { useRequest, useToggle } from 'hooks'
import { useHeaderFiltersStore } from 'features/HeaderFilters' import { useHeaderFiltersStore } from 'features/HeaderFilters'
import { useLexicsStore } from 'features/LexicsStore' import { useLexicsStore } from 'features/LexicsStore'
@ -26,25 +27,26 @@ export const useTournamentFilter = () => {
isOpen, isOpen,
open, open,
} = useToggle() } = useToggle()
const {
isFetching,
request: requestTournaments,
} = useRequest(getSportTournaments)
const [page, setPage] = useState(0)
useEffect(() => { useEffect(() => {
setList([]) setList([])
getSportTournaments(selectedSportTypeId).then(setList) requestTournaments(selectedSportTypeId, 0).then((tournaments) => {
}, [selectedSportTypeId]) setList(tournaments)
setPage(1)
const [requestSent, setRequestSent] = useState(false) })
}, [selectedSportTypeId, requestTournaments])
const handleScroll = (e: BaseSyntheticEvent) => { const fetchMore = async () => {
const scrollPositionBottom = e.target.scrollHeight - e.target.scrollTop if (isFetching) return
- e.target.clientHeight - 400 <= 0
if (scrollPositionBottom && !requestSent) { const newTournaments = await requestTournaments(selectedSportTypeId, page)
setRequestSent(true) setList([...list, ...newTournaments])
getSportTournaments(selectedSportTypeId).then((res) => { setPage(page + 1)
setList([...list, ...res])
setRequestSent(false)
})
}
} }
const tournaments = normalizeTournaments(list, suffix) const tournaments = normalizeTournaments(list, suffix)
@ -66,7 +68,7 @@ export const useTournamentFilter = () => {
return { return {
close, close,
handleScroll, fetchMore,
isOpen, isOpen,
onResetSelectedTournament, onResetSelectedTournament,
onTournamentSelect, onTournamentSelect,

@ -7,7 +7,7 @@ import { TournamentList } from '../TournamentList'
import { useTournamentFilter } from './hooks' import { useTournamentFilter } from './hooks'
import { import {
Wrapper, Wrapper,
ListWrapper, InfiniteScroll,
DropdownButton, DropdownButton,
ButtonTitle, ButtonTitle,
Arrows, Arrows,
@ -17,7 +17,7 @@ import {
export const TournamentFilter = () => { export const TournamentFilter = () => {
const { const {
close, close,
handleScroll, fetchMore,
isOpen, isOpen,
onResetSelectedTournament, onResetSelectedTournament,
onTournamentSelect, onTournamentSelect,
@ -50,12 +50,12 @@ export const TournamentFilter = () => {
{ {
isOpen && ( isOpen && (
<OutsideClick onClick={close}> <OutsideClick onClick={close}>
<ListWrapper id='tournamentsList' onScroll={handleScroll}> <InfiniteScroll onFetchMore={fetchMore}>
<TournamentList <TournamentList
tournaments={tournaments} tournaments={tournaments}
onSelect={onTournamentSelect} onSelect={onTournamentSelect}
/> />
</ListWrapper> </InfiniteScroll>
</OutsideClick> </OutsideClick>
) )
} }

@ -2,8 +2,9 @@ import styled from 'styled-components/macro'
import { devices } from 'config/devices' import { devices } from 'config/devices'
import { customScrollbar } from 'features/Common' import { customScrollbar } from 'features/Common'
import { InfiniteScroll as InfiniteScrollBase } from 'features/InfiniteScroll'
export const ListWrapper = styled.div` export const InfiniteScroll = styled(InfiniteScrollBase)`
position: absolute; position: absolute;
left: 0; left: 0;
top: calc(100% + 8px); top: calc(100% + 8px);

@ -1,10 +1,9 @@
import React from 'react' import React, { SyntheticEvent } from 'react'
import map from 'lodash/map' import map from 'lodash/map'
import { SportTypes } from 'config' import { SportTypes } from 'config'
import { useItemsList } from 'features/ItemsList/hooks'
import { import {
Logo, Logo,
LogoWrapper, LogoWrapper,
@ -31,14 +30,13 @@ type TournamentListProps = {
tournaments: Array<Tournament>, tournaments: Array<Tournament>,
} }
export const TournamentList = ({ onSelect, tournaments }: TournamentListProps) => { const onError = (fallbackSrc: string) => (e: SyntheticEvent<HTMLImageElement>) => {
const { // eslint-disable-next-line no-param-reassign
onError, e.currentTarget.src = fallbackSrc
ref, }
} = useItemsList()
return ( export const TournamentList = ({ onSelect, tournaments }: TournamentListProps) => (
<Wrapper ref={ref} role='listbox'> <Wrapper role='listbox'>
{map(tournaments, ({ {map(tournaments, ({
country, country,
fallbackImage, fallbackImage,
@ -54,7 +52,7 @@ export const TournamentList = ({ onSelect, tournaments }: TournamentListProps) =
> >
<LogoWrapper> <LogoWrapper>
<Logo <Logo
data-src={logo} src={logo}
onError={onError(fallbackImage)} onError={onError(fallbackImage)}
alt={name} alt={name}
/> />
@ -67,5 +65,4 @@ export const TournamentList = ({ onSelect, tournaments }: TournamentListProps) =
</ListItem> </ListItem>
))} ))}
</Wrapper> </Wrapper>
) )
}

@ -1,6 +1,9 @@
import { useMemo, useCallback } from 'react' import {
useMemo,
useState,
useCallback,
} from 'react'
import isNumber from 'lodash/isNumber'
import format from 'date-fns/format' import format from 'date-fns/format'
import startOfDay from 'date-fns/startOfDay' import startOfDay from 'date-fns/startOfDay'
@ -60,11 +63,7 @@ export const useFilters = () => {
const [ const [
selectedTournamentId, selectedTournamentId,
setSelectedTournamentId, setSelectedTournamentId,
] = useQueryParamStore<number | null>({ ] = useState<number | null>(null)
defaultValue: null,
key: filterKeys.TOURNAMENT_ID,
validator: isNumber,
})
const store = useMemo(() => ({ const store = useMemo(() => ({
selectedDate, selectedDate,

@ -19,14 +19,14 @@ export const useIntersectionObserver = ({ onIntersect, options }: Args) => {
const callbackRef = useRef(onIntersect) const callbackRef = useRef(onIntersect)
const rootRef = useRef<HTMLDivElement>(null) const rootRef = useRef<HTMLDivElement>(null)
const targetRef = useRef<HTMLDivElement>(null) const targetRef = useRef<HTMLDivElement>(null)
const rootElement = rootRef.current
const targetElement = targetRef.current
useEffect(() => { useEffect(() => {
callbackRef.current = onIntersect callbackRef.current = onIntersect
}, [onIntersect]) }, [onIntersect])
useEffect(() => { useEffect(() => {
const rootElement = rootRef.current
const targetElement = targetRef.current
if (!targetElement) { if (!targetElement) {
return undefined return undefined
} }
@ -44,11 +44,7 @@ export const useIntersectionObserver = ({ onIntersect, options }: Args) => {
return () => { return () => {
observer.disconnect() observer.disconnect()
} }
}, [ }, [options])
rootElement,
targetElement,
options,
])
return { rootRef, targetRef } return { rootRef, targetRef }
} }

@ -6,22 +6,31 @@ import { Root, Target } from './styled'
type Props = { type Props = {
children: ReactNode, children: ReactNode,
className?: string,
fullPageScroll?: boolean,
onFetchMore: () => void, onFetchMore: () => void,
options?: IntersectionObserverInit, options?: IntersectionObserverInit,
} }
export const InfiniteScroll = ({ export const InfiniteScroll = ({
children, children,
className,
fullPageScroll,
onFetchMore, onFetchMore,
options, options,
}: Props) => { }: Props) => {
const { targetRef } = useIntersectionObserver({ const { rootRef, targetRef } = useIntersectionObserver({
onIntersect: onFetchMore, onIntersect: onFetchMore,
options, options,
}) })
return ( return (
<Root> <Root
// игнорируем rootRef, чтобы отслеживать пересечение target
// с областью видимости document, т.е. всей страницы
ref={fullPageScroll ? null : rootRef}
className={className}
>
{children} {children}
<Target ref={targetRef} /> <Target ref={targetRef} />
</Root> </Root>

@ -44,7 +44,7 @@ export const Matches = (props: Props) => {
} }
return ( return (
<InfiniteScroll onFetchMore={fetchMoreMatches}> <InfiniteScroll fullPageScroll onFetchMore={fetchMoreMatches}>
<MatchesList <MatchesList
as='grid' as='grid'
title='broadcast' title='broadcast'

@ -27,15 +27,17 @@ export type Tournament = {
export type Tournaments = Array<Tournament> export type Tournaments = Array<Tournament>
const LIMIT = 20
export const getSportTournaments = ( export const getSportTournaments = (
sportId?: number | null, sportId: SportTypes | null,
offset: number = 0, page: number = 0,
): Promise<Tournaments> => { ): Promise<Tournaments> => {
const config = { const config = {
body: { body: {
params: { params: {
_p_limit: 20, _p_limit: LIMIT,
_p_offset: offset, _p_offset: page * LIMIT,
_p_sport: sportId, _p_sport: sportId,
}, },
proc, proc,

Loading…
Cancel
Save