我有一个具有分页功能的 API 端点。我需要实现一个 AutoComplete 组件(使用 MUI5 AutoComplete 组件),每当我到达列表末尾时,它都会获取下一页的数据并将结果与上一个结果连接起来。
我在网上搜索过,但没有找到任何有用的结果。
我发现this类似的问题在两年多前就被问过,但直到今天还没有得到解答。
与我最接近的问题是这个,它也不够完整,我无法找到使用它的方法。
我真的很感谢您提供的任何帮助。
尝试与 react-infinite-scroll-hook 结合
类似这样的:
const [sentryRef, { rootRef }] = useInfiniteScroll({
loading: isLoading,
hasNextPage,
disabled: !open,
onLoadMore: () => debouncedOptionsLoad(),
});
...
const renderSentry = () => {
return hasNextPage || isLoading ? (
<ListItem ref={sentryRef} key="sentry">
Loading...
</ListItem>
) : null;
};
...
const handleRenderOption = (optionProps, option) => {
const { id, label } = option;
if (id === "sentry") {
return renderSentry();
}
return (
<ListItem {...optionProps} key={id}>
//for highlighting input string
<Hightlight value={label} filter={searchString} />
</ListItem>
);
};
...
const sentryOption = { id: "sentry", label: '' };
...
return (
<MuiAutocomplete
...some props
//rootRef from useInfiniteScroll hook
ListboxProps={{ ref: rootRef }},
filterOptions={(filterOptions) => {
//filterOptions need for adding sentry to your options
const shouldRenderSentry = meta.hasNextPage;
if (shouldRenderSentry) {
filterOptions.push(sentryOption);
}
return filterOptions;
}},
renderInput={(inputProps) => (
<TextField
value={searchString}
onChange={ //function for change "searchString" and refetch your data }
/>
)}
...
/>
所以,我正在使用我自己提供的组件。它可能有一些问题,但基本功能绝对有效。当用户滚动到下拉列表的底部时,会出现一个加载器,并从后端获取下一页选项。我将 Redux 与 RTK 查询结合使用,因此新的选项页面将附加到减速器中的现有选项中,如下所示:
builder.addMatcher(inventoryService.endpoints.searchSuppliers.matchFulfilled, (state, action) => {
if (['success', 'info'].includes(action.payload?.type as ResponseType) && action.payload?.data?.suppliers) {
if (action.payload?.data?.currentPage > 1) {
const { suppliers, ...rest } = action.payload.data
state.searchSuppliers = {
suppliers: [...state.searchSuppliers.suppliers, ...suppliers],
...rest
}
} else state.searchSuppliers = action.payload.data
}
})
代码位于 Typescript 中,但我通常不会经常定义类型,因为我基本上使用 Javascript,所以请注意您在代码中看到的任何类型。如果需要,您可以定义类型。这是组件:
/* eslint-disable no-unused-vars */
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { UseFormSetValue } from 'react-hook-form'
import { Autocomplete, Box, CircularProgress, Paper, Typography } from '@mui/material'
import { IconCheck, IconChevronDown, IconPlus } from '@tabler/icons-react'
import { debounce, unionBy } from 'lodash'
import { AvatarWithLabel } from '../AvatarWithLabel'
import { CustomErrors } from '../CustomErrors'
import { CustomOutlinedInput } from '../CustomOutlinedInput'
export interface AvatarSearchDropdownProps {
heading?: React.ReactNode
headingClass?: string
gap?: string | number
width?: string | number
height?: string | number
color?: string
fontSize?: string | number
lineHeight?: string | number
fontWeight?: number | string
padding?: string | number
borderRadius?: string | number
value?: any
valueKey: string
isRequired?: boolean
placeholder?: string
name: string
error?: string
totalPages: number
options: any[]
menuHeader?: string
resourceName?: string
onNewItemClick?: any
isOptionEqualToValue: any
setValue?: UseFormSetValue<any>
onChange?: (value: any) => void
getAvatarAttributes?: any
getData: (search: string, page: number) => void
getOptionLabel: any
isLoadingOptions?: boolean
}
const DropdownItem = ({
index,
totalOptions,
fontSize,
fontWeight,
lineHeight,
getAvatarAttributes,
getOptionLabel,
option,
selected,
lastOptionRef
}: {
index: number
totalOptions: number
fontSize?: string | number
lineHeight?: string | number
fontWeight?: number | string
getAvatarAttributes?: any
getOptionLabel: any
option: any
selected: boolean
lastOptionRef: (node: any) => void
}) => (
<Typography
ref={index === totalOptions - 1 ? lastOptionRef : null}
width="100%"
display="flex"
justifyContent="space-between"
alignItems="center"
gap="4px"
fontSize={fontSize}
lineHeight={lineHeight}
fontWeight={fontWeight}
color={selected ? '#1F69FF' : '#353A46'}>
{getAvatarAttributes ? (
<AvatarWithLabel
label={getAvatarAttributes(option)?.label}
subLabel={getAvatarAttributes(option)?.subLabel}
imgSrc={getAvatarAttributes(option)?.imgSrc}
/>
) : (
getOptionLabel(option)
)}
{selected && <IconCheck color="#1F69FF" size={lineHeight} />}
</Typography>
)
export const AvatarSearchDropdown = (props: AvatarSearchDropdownProps) => {
const {
heading,
headingClass,
gap = '8px',
width = '100%',
height = '48px',
color = '#353A46',
fontSize = '16px',
lineHeight = '24px',
fontWeight = 400,
padding = '12px 12px',
borderRadius = '8px',
value,
valueKey,
isRequired,
totalPages,
placeholder,
menuHeader,
resourceName,
onNewItemClick,
name,
error,
options,
isOptionEqualToValue,
setValue,
onChange,
getAvatarAttributes,
getOptionLabel,
getData,
isLoadingOptions
} = props
const [searchTerm, setSearchTerm] = useState('')
const [page, setPage] = useState(1)
const observer = useRef<IntersectionObserver | null>(null)
const handleSearchChange = useCallback(
debounce((newSearchTerm: string, getData: any) => {
setPage(1)
getData(newSearchTerm, 1)
}, 500),
[]
)
const handleInputChange = (event: any) => {
const newSearchTerm = event?.target?.value || ''
setSearchTerm(newSearchTerm)
handleSearchChange(newSearchTerm, getData)
}
const handleSelect = (_: any, option: any) => {
onChange
? onChange(option)
: setValue && setValue(name, option, { shouldDirty: true, shouldValidate: true, shouldTouch: true })
}
const updatedOptions = useMemo(() => {
return value ? unionBy(options ?? [], [value], valueKey) : (options ?? [])
}, [options, value, valueKey])
const lastOptionRef = useCallback(
(node: any) => {
if (isLoadingOptions) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && page < totalPages) {
getData(searchTerm, page + 1)
setPage(page + 1)
}
})
if (node) observer.current.observe(node)
},
[isLoadingOptions, page, totalPages, getData, searchTerm]
)
const CustomPaper = useMemo(() => {
const Component = (props: any) => {
return (
<Paper
{...props}
sx={{
borderRadius: '8px',
'& .MuiAutocomplete-option': {
margin: '8px 4px',
borderRadius: '8px'
},
'& .Mui-focused .MuiTypography-root': {
color: '#1F69FF'
}
}}
className="thin-scrollbar-class">
<Box>
{menuHeader && (
<Typography p="16px 16px 8px 16px" fontSize={fontSize} lineHeight={lineHeight} fontWeight={fontWeight}>
{menuHeader}
</Typography>
)}
{props.children}
{resourceName && onNewItemClick && (
<Typography
onMouseDown={onNewItemClick}
display="flex"
p="12px"
className="cursor-pointer border-t border-[#D1D4DC] hover:bg-slate-100"
fontSize={fontSize}
lineHeight={lineHeight}
fontWeight={fontWeight}
color="#1F69FF">
<IconPlus /> Add New {resourceName}
</Typography>
)}
</Box>
</Paper>
)
}
// Set the display name
Component.displayName = 'CustomPaper'
return Component
}, [menuHeader, fontSize, lineHeight, fontWeight, resourceName, onNewItemClick])
return (
<Box display="flex" flexDirection="column" gap={gap} width={width}>
{heading && (
<Typography variant="body2" className={headingClass}>
{heading} {isRequired && <span className="text-[#7AA7FF]">*</span>}
</Typography>
)}
<Autocomplete
value={value}
fullWidth
options={updatedOptions}
getOptionLabel={getOptionLabel}
onChange={handleSelect}
onInputChange={handleInputChange}
noOptionsText={isLoadingOptions ? <CircularProgress size={lineHeight} /> : 'No results found'}
isOptionEqualToValue={isOptionEqualToValue}
renderOption={(props, option, { selected, index }) => {
const { key, ...optionProps } = props
return (
<li className="!px-0 !py-0 !mx-0" key={key} {...optionProps}>
{isLoadingOptions && index === updatedOptions.length - 1 ? (
<Box className="w-full flex gap-2 flex-col items-center">
<DropdownItem
index={index}
totalOptions={updatedOptions.length}
fontSize={fontSize}
fontWeight={fontWeight}
lineHeight={lineHeight}
lastOptionRef={lastOptionRef}
getOptionLabel={getOptionLabel}
getAvatarAttributes={getAvatarAttributes}
option={option}
selected={selected}
/>
<CircularProgress size={lineHeight} />
</Box>
) : (
<DropdownItem
index={index}
totalOptions={updatedOptions.length}
fontSize={fontSize}
fontWeight={fontWeight}
lineHeight={lineHeight}
lastOptionRef={lastOptionRef}
getOptionLabel={getOptionLabel}
getAvatarAttributes={getAvatarAttributes}
option={option}
selected={selected}
/>
)}
</li>
)
}}
renderInput={(params) => (
<CustomOutlinedInput
{...params}
height={height}
fontSize={fontSize}
lineHeight={lineHeight}
placeholder={placeholder}
padding={padding}
borderRadius={borderRadius}
color={color}
/>
)}
PaperComponent={CustomPaper}
popupIcon={<IconChevronDown />}
/>
<CustomErrors margin="0px 12px" errors={error} />
</Box>
)
}
如有任何疑问,请随时联系。