RTK查询-无限滚动,保留现有数据

Kwe*_*Dev 19 react-redux redux-toolkit rtk-query

我正在尝试在我们当前的应用程序中实现无限滚动功能;

我们首先获取页面的前 5 个“帖子”。滚动到页面底部后,我们会获取接下来的 5 篇帖子。

这很好用,但是使用相同的查询意味着现有数据(前 5 个帖子)已被新数据替换。

是否可以将现有数据与新数据合并?

我可以将它们合并到位,例如使用类似的内容;const posts = [newPosts, oldPosts]但如果现有数据被修改,我们就会失去 RTK 查询提供的数据失效。

对于这种情况,推荐的方法是什么?

Daa*_*ijn 31

在 RTK 1.9 中,现在可以使用该merge选项将新获取的数据与当前位于缓存内的数据合并。确保将该选项与serializeQueryArgs或一起使用forceRefetch以保留数据的缓存条目。

createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  endpoints: (build) => ({
    listItems: build.query<string[], number>({
      query: (pageNumber) => `/listItems?page=${pageNumber}`,
      // Only have one cache entry because the arg always maps to one string
      serializeQueryArgs: ({ endpointName }) => {
        return endpointName
      },
      // Always merge incoming data to the cache entry
      merge: (currentCache, newItems) => {
        currentCache.push(...newItems)
      },
      // Refetch when the page arg changes
      forceRefetch({ currentArg, previousArg }) {
        return currentArg !== previousArg
      },
    }),
  }),
})
Run Code Online (Sandbox Code Playgroud)

来源:关于合并选项的 RTK 文档

使用这个你可以轻松实现无限滚动。更改pageNumber查询的参数,将自动获取新数据并将其与缓存中已有的数据连接起来。

为了说明这一点,我在CodeSandbox上创建了一个工作示例。

在此输入图像描述

  • 这是一个痛苦的**,他们已经用 RTK 做了所有的事情,但他们只是不想解决这个问题,我不明白为什么他们不想解决这个问题并做出类似的反应查询 (4认同)
  • 这节省了很多逻辑和很多行代码,非常感谢 (2认同)
  • 您现在可能想知道的是:如果我真的想覆盖整个缓存怎么办?不太漂亮,但是您可以向合并函数添加一些逻辑,在某些情况下它不会合并新旧缓存。对于我的用例,我添加了一个 if 语句来检查用户是否正在请求第一页(例如 pageNumber === 0)。每当请求第一页时,我都会用第一页数据替换完整缓存。每当请求另一个页面(例如 pageNumber &gt; 0)时,我会将其合并到现有缓存中。 (2认同)
  • @Hamed:太晚了,但是“不想解决问题”是什么意思?具体是_哪个_问题?请注意,我们无法监控每个 SO 问题以获取想法。如果您有建议,最好在我们的存储库上提出问题。 (2认同)

Ehs*_*ari 1

这是一种利用 rtk-query 的缓存优势实现无限加载的解决方法

在我的示例中,后端响应类型是

{ items: anyDTO[], count: number /* totalCount */ }

为了使其在使标签无效时正常工作,我必须使用钩子获取第一页并在 useEffect 中处理其余部分。

import { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import * as R from 'ramda';
import { ApiEndpointQuery } from '@reduxjs/toolkit/dist/query/core/module';
import { QueryHooks } from '@reduxjs/toolkit/dist/query/react/buildHooks';

interface UseLazeyInfiniteDataProps<T, N> {
  api: T;
  /** any rtk-query api: passing the whole enpoint so we have access to api utils to invalidate provided tags */
  apiEndpointName: N;
  /** apiEndpoint name to retrieve correct apiEndpoint query which will have 'initiate' and 'useQuery' */
  apiArgs: { [key: string]: any; params: object };
  /** apiArgs are the query arguments it should have a params objec */
  limit?: number;
  /** limit or page-size per request (defaults 20) */
  invalidatesTags?: any[];
}
/**
 * This hook is for having infinite loading experience with caching posibility of rtk-query
 * it's storing the data comming from rtk-q to local useState throgh a useEffect hook
 * in orther to make it work when invalidating tags it makes the first page request through rtk-query hook
 * and whenever it changes it will refetch the rest data
 */
const useLazyInfiniteData = <
  T extends { endpoints: any; util: any },
  N extends keyof T['endpoints'],
>({
  api,
  apiEndpointName,
  apiArgs,
  limit = 20,
  invalidatesTags,
}: UseLazeyInfiniteDataProps<T, N>) => {
  const dispatch = useDispatch<any>();
  const [pageNumber, setPageNumber] = useState(1); // first load only page 1
  const [maxPage, setMaxPage] = useState(0); // we don't know how many pages could exists yet
  const [accData, setAccData] = useState<any[]>([]);
  const [isFetchingMore, setIsFetchingMore] = useState(false);

  const apiEndpoint: ApiEndpointQuery<any, any> & QueryHooks<any> =
    api.endpoints[apiEndpointName];
  // we need this extra hook to automate refetching when invalidating tag
  // this will make the useEffect rerender if the first page data changes
  const {
    currentData: firstPageData,
    isLoading,
    isFetching,
    refetch: refetch_,
  } = apiEndpoint.useQuery({
    ...apiArgs,
    params: R.mergeRight(apiArgs.params, { offset: 0, limit }),
  });

  const refetch = useCallback(() => {
    if (invalidatesTags) {
      dispatch(api.util.invalidateTags());
    }
    refetch_();
  }, [api.util, dispatch, invalidatesTags, refetch_]);

  /** when params change like changing filters in the params then we reset the loading pages to 1 */
  useEffect(
    function resetPageLoadDataForSinglePage() {
      setPageNumber(1);
    },
    [apiArgs.params],
  );

  useEffect(
    function loadMoreDataOnPageNumberIncrease() {
      if (firstPageData)
        setMaxPage(Math.ceil((firstPageData as any).count / limit));

      if (pageNumber === 1) {
        setAccData((firstPageData as any)?.items ?? []);
      }
      if (pageNumber > 1) {
        setIsFetchingMore(true);
        const promises = R.range(1, pageNumber).map((page) =>
          dispatch(
            apiEndpoint.initiate({
              ...apiArgs,
              params: R.mergeRight(apiArgs.params, {
                offset: page * limit,
                limit,
              }),
            }),
          ).unwrap(),
        );

        Promise.all(promises)
          .then((data: any[]) => {
            const items = R.chain(R.propOr([], 'items'), [
              firstPageData,
              ...data,
            ]);
            setAccData(items);
          })
          .catch(console.error)
          .finally(() => {
            setIsFetchingMore(false);
          });
      }
    },
    [apiEndpoint, apiArgs, dispatch, firstPageData, limit, pageNumber],
  );

  /** increasing pageNumber will make the useEffect run */
  const loadMore = useCallback(() => {
    setPageNumber(R.inc);
  }, []);

  return {
    data: accData,
    loadMore,
    hasMore: pageNumber < maxPage,
    isLoading,
    isFetching,
    isFetchingMore,
    refetch,
  };
};

export default useLazyInfiniteData;
Run Code Online (Sandbox Code Playgroud)

用法:假设你有rtk查询API:

const extendedApi = emptySplitApi.injectEndpoints({ 
  endpoints: (build) => ({ 
    example: build.query({
      query: ({x, params: { offset, limit }}) => 'test'
    })
  }),
})
Run Code Online (Sandbox Code Playgroud)

您可以像这样使用它:

useLazyInfiniteData({ 
  api: extendedApi,
  apiEndpointName: 'example',
  apiArgs: { x }, // better to be a memorized value
})
Run Code Online (Sandbox Code Playgroud)