reduxjs / redux-toolkit

The official, opinionated, batteries-included toolset for efficient Redux development
https://redux-toolkit.js.org
MIT License
10.55k stars 1.13k forks source link

Issue: Circular Dependency in RTK Query with Code-Splitting Endpoints #4427

Open krakenfuss opened 1 month ago

krakenfuss commented 1 month ago

We are building an RTK-powered Redux store and encountered an issue with accessing api.util.updateQueryData generated by createApi inside an endpoint during API initialization. The manual suggests using the following approach:

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    getPost: build.query<Post, number>({
      query: (id) => `post/${id}`,
      providesTags: ['Post'],
    }),
    updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
      query: ({ id, ...patch }) => ({
        url: `post/${id}`,
        method: 'PATCH',
        body: patch,
      }),
      async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
        const patchResult = dispatch(
          api.util.updateQueryData('getPost', id, (draft) => {
            Object.assign(draft, patch);
          })
        );
        try {
          await queryFulfilled;
        } catch {
          patchResult.undo();
        }
      },
    }),
  }),
});

The example above shows a self-reference of api inside the initialisation of itself which is only possible if you write the endpoint definitions inside of createApi.

Our goal instead is to avoid writing lengthy files like proposed and rather use code-splitting for endpoints defined in separate files from the API definition. For example:

Problem

With this setup, there is no safe way to access the generated api inside these files without creating either a circular dependency or encountering an error like is referenced directly or indirectly in its own initializer.

Example of Circular Dependency

File A

import { updatePost } from './fileB';
const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    ...updatePost(build),
  }),
});

File B

import { api } from './fileA';
export function updatePost(build) {
  return {
    query: ({ id, ...patch }) => ({
      url: `post/${id}`,
      method: 'PATCH',
      body: patch,
    }),
    async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
      const patchResult = dispatch(
        api.util.updateQueryData('getPost', id, (draft) => {
          Object.assign(draft, patch);
        })
      );
      try {
        await queryFulfilled;
      } catch {
        patchResult.undo();
      }
    },
  };
}

Desired Solution

Ideally, endpoints would receive the api via dependency injection. However, we cannot do this without modifying createApi, as injecting api during its initialization is not possible in JavaScript.

Current Attempt

import { updatePost } from './fileB';
const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  tagTypes: ['Post'],
  endpoints: (build) => ({
    ...updatePost(build, api), // Raises "is referenced directly or indirectly in its own initializer"
  }),
});

Proposed Solution

Would it be possible to inject api into the endpoints setup callback, like so?

Desired API Initialization

import { updatePost } from './fileB';
const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  tagTypes: ['Post'],
  endpoints: (build, internalApi) => ({ // Injected API or API-Getter from createApi itself
    ...updatePost(build, internalApi), 
  }),
});

This approach would allow for a more modular setup without the risk of circular dependencies. Is this a feasible enhancement to createApi?

phryneas commented 1 month ago

We have injectEndpoints for that, please don't invent your own code splitting patterns.

https://redux-toolkit.js.org/rtk-query/usage/code-splitting

krakenfuss commented 1 month ago

Thank you for your response!

I came across injectEndpoints but had the impression that it is more of a workaround than a suitable mechanism for code splitting larger APIs, as it requires continuously updating and extending existing APIs. However, I understand that you might have chosen this approach (and the larger endpoint definition in one file) to maintain type inference of the self-referenced API, correct?

Anyways, thank you for this great tool. I really appreciate your work on it.

Please feel free to close this issue.

lamhungypl commented 1 month ago

the only issue I have so far with injectedEnpoint is that you don't have the TagTypes across your injectedApi's addTagTypes. A temporal workaround is providing external constants types and import to injectedApi.

phryneas commented 1 month ago

If you have tagTypes that you share between apis, you can still define those in the initial createApi call. If they are unique to one api, you can keep it inside of that. If they are shared between e.g. two apis, you could still choose to stack one on top of the other.