Search code examples
react-reduxredux-toolkit

Memoized reselect selector with RTKQuery selectFromResult still rerenders component


I'm working with an API endpoint that returns random data for a particular property (unstableProperty) every time it is called for the same entity. This behavior is illustrated below:

// First API Call
GET /entities/3
{
  "name": "Entity Name",
  "message": "This is some entity description",
  "files": [
    { "fileName": "File1", "unstableProperty": "bbserg" },
    { "fileName": "File2", "unstableProperty": "thslkuygseaf" }
  ]
}

// Second API Call (5 seconds later)
GET /entities/3
{
  "name": "Entity Name",
  "message": "This is some entity description",
  "files": [
    { "fileName": "File1", "unstableProperty": "ystgrgazazrg" },
    { "fileName": "File2", "unstableProperty": "strhsryjarehaerh" }
  ]
}

Context

A business rule requires polling this endpoint every X seconds for changes. However, I do not want my component to refresh every time as it includes a table with potentially hundreds of rows, and each row includes a thumbnail. Rerendering the table each time causes performance issues and makes the thumbnails flicker.

Issue

I'm attempting to use this example from RTKQuery documentation to prevent rerendering but it looks like this does not work as intended. Despite setting up a selector that filters out the unstable properties, the component still rerenders every polling interval.

I prepared a detailed example on this codesandbox: https://codesandbox.io/p/sandbox/modest-snyder-vyxkxd?file=%252Fsrc%252Fapi%252FbaseApi.ts

Here is how I have simulated the unstable property for demonstration:

let calls = 0;

export const baseApi = createApi({
  baseQuery: fetchBaseQuery(),
  endpoints: (build) => ({
    getTestData: build.query<TestDataResponse, void>({
      queryFn: () => ({
        data: {
          name: "Entity Name",
          message: "This is some entity description",
          files: [
            { fileName: "File1", unstableProperty: `value_${calls++}` },
            { fileName: "File2", unstableProperty: `value_${calls++}` },
          ],
        },
      }),
    }),
  }),
});

My component that shows the issue utilizes this query in the following way:

  const selectOnlyStableData = useMemo(() => {
    return createSelector(
      (data: TestDataResponse | undefined) => data,
      (data: TestDataResponse | undefined) => {
        if (!data) return undefined;

        // Select everything from data except files' unstableProperty
        const stableData = {
          name: data.name,
          message: data.message,
          files: data.files.map((f) => ({
            fileName: f.fileName,
          })),
        };

        return stableData;
      }
    );
  }, []);

  const { data } = useGetTestDataQuery(undefined, {
    pollingInterval: 5000,
    selectFromResult: ({ data }) => ({
      data: selectOnlyStableData(data),
    }),
  });

Despite the selector, the component rerenders every 5 seconds. Everything works as intended if I remove the unstableProperty from the query result entirely.

Question

How can I prevent rerenders caused by changes to unstableProperty while still polling for other data changes using RTK Query?

Please note I can't simply remove this unstable property (e.g., in the transformResponse) because I need the value of this unstable property in other components.


Solution

  • There is, a bit hidden in the docs if you ask me, resultEqualityCheck option. It is mentioned here in the example at the very bottom of the page and briefly documented here. This property does exactly what I need - compares current and previous output and returns new reference only when they are not identical. From documentation:

    If provided, used to compare a newly generated output value against previous values in the cache. If a match is found, the old value is returned. This addresses the common todos.map(todo => todo.id) use case, where an update to another field in the original data causes a recalculation due to changed references, but the output is still effectively the same.

    The fixed selector looks as following:

    const selectOnlyStableData = useMemo(() => {
        return createSelector(
          (data: TestDataResponse | undefined) => data.data,
          (data: TestDataResponse | undefined) => {
            if (!data) return undefined;
    
            // Select everything from data except files' unstableProperty
            const stableData = {
              name: data.name,
              message: data.message,
              files: data.files.map((f) => ({
                fileName: f.fileName,
              })),
            };
    
            return stableData;
          },
          {
            memoizeOptions: {
              resultEqualityCheck: deepEqual,
            },
          }
        );
      }, []);
    

    Fixed codesandbox: https://codesandbox.io/p/sandbox/agitated-hugle-xtyt78?file=%252Fsrc%252Fcomponents%252FProblem.tsx