diff --git a/.changeset/unlucky-rules-complain.md b/.changeset/unlucky-rules-complain.md new file mode 100644 index 000000000000..5f3986d0b76c --- /dev/null +++ b/.changeset/unlucky-rules-complain.md @@ -0,0 +1,11 @@ +--- +"@refinedev/core": patch +--- + +fix(core): add missing checks and warnings for `ids` and `resource` props in `useMany` hook + +Added checks for `ids` and `resource` props to check in runtime if they are valid or not. + +`useMany` will warn if `ids` or `resource` props are missing unless the query is manually enabled through `queryOptions.enabled` prop. + +[Resolves #6617](https://github.com/refinedev/refine/issues/6617) diff --git a/packages/core/src/hooks/data/useMany.spec.tsx b/packages/core/src/hooks/data/useMany.spec.tsx index 441e0a05f023..a5cb90579f10 100644 --- a/packages/core/src/hooks/data/useMany.spec.tsx +++ b/packages/core/src/hooks/data/useMany.spec.tsx @@ -10,6 +10,8 @@ import { import type { IRefineContextProvider } from "../../contexts/refine/types"; import { useMany } from "./useMany"; +import type { BaseKey } from "@contexts/data/types"; +import * as warnOnce from "warn-once"; const mockRefineProvider: IRefineContextProvider = { hasDashboard: false, @@ -963,4 +965,113 @@ describe("useMany Hook", () => { ); }); }); + + describe("should require `ids` and `resource` props", () => { + it("should require `ids` prop", () => { + const warnMock = jest.spyOn(console, "warn").mockImplementation(() => {}); + + const getManyMock = jest.fn().mockResolvedValue({ + data: [], + }); + + const result = renderHook( + () => + useMany({ + resource: "posts", + ids: undefined as unknown as BaseKey[], + }), + { + wrapper: TestWrapper({ + dataProvider: { + default: { + ...MockJSONServer.default, + getMany: getManyMock, + }, + }, + resources: [{ name: "posts" }], + }), + }, + ); + + expect(result.result.current.isLoading).toBeTruthy(); + expect(result.result.current.fetchStatus).toBe("idle"); + expect(getManyMock).not.toHaveBeenCalled(); + expect(warnMock).toHaveBeenCalledWith( + expect.stringContaining('[useMany]: Missing "ids" prop.'), + ); + + warnMock.mockClear(); + }); + + it("should require `resource` prop", () => { + const warnMock = jest.spyOn(console, "warn").mockImplementation(() => {}); + + const getManyMock = jest.fn().mockResolvedValue({ + data: [], + }); + + const result = renderHook( + () => + useMany({ + resource: undefined as unknown as string, + ids: ["1", "2"], + }), + { + wrapper: TestWrapper({ + dataProvider: { + default: { + ...MockJSONServer.default, + getMany: getManyMock, + }, + }, + resources: [{ name: "posts" }], + }), + }, + ); + + expect(result.result.current.isLoading).toBeTruthy(); + expect(result.result.current.fetchStatus).toBe("idle"); + expect(getManyMock).not.toHaveBeenCalled(); + expect(warnMock).toHaveBeenCalledWith( + expect.stringContaining('[useMany]: Missing "resource" prop.'), + ); + + warnMock.mockClear(); + }); + + it("should not warn if manually enabled", () => { + const warnMock = jest.spyOn(console, "warn").mockImplementation(() => {}); + + const getManyMock = jest.fn().mockResolvedValue({ + data: [], + }); + + const result = renderHook( + () => + useMany({ + resource: undefined as unknown as string, + ids: ["1", "2"], + queryOptions: { + enabled: true, + }, + }), + { + wrapper: TestWrapper({ + dataProvider: { + default: { + ...MockJSONServer.default, + getMany: getManyMock, + }, + }, + resources: [{ name: "posts" }], + }), + }, + ); + + expect(result.result.current.isLoading).toBeTruthy(); + expect(result.result.current.fetchStatus).toBe("fetching"); + expect(getManyMock).toHaveBeenCalled(); + expect(warnMock).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/core/src/hooks/data/useMany.ts b/packages/core/src/hooks/data/useMany.ts index e27639630972..ac69ad8ec975 100644 --- a/packages/core/src/hooks/data/useMany.ts +++ b/packages/core/src/hooks/data/useMany.ts @@ -37,6 +37,7 @@ import { type UseLoadingOvertimeReturnType, useLoadingOvertime, } from "../useLoadingOvertime"; +import warnOnce from "warn-once"; export type UseManyProps = { /** @@ -133,17 +134,24 @@ export const useMany = < const combinedMeta = getMeta({ resource, meta: preferredMeta }); + const hasIds = Array.isArray(ids); + const hasResource = Boolean(resource?.name); + const manuallyEnabled = queryOptions?.enabled === true; + + warnOnce(!hasIds && !manuallyEnabled, idsWarningMessage(ids, resource?.name)); + warnOnce(!hasResource && !manuallyEnabled, resourceWarningMessage()); + useResourceSubscription({ resource: identifier, types: ["*"], params: { - ids: ids, + ids: ids ?? [], meta: combinedMeta, metaData: combinedMeta, subscriptionType: "useMany", ...liveParams, }, - channel: `resources/${resource.name}`, + channel: `resources/${resource?.name ?? ""}`, enabled: isEnabled, liveMode, onLiveEvent, @@ -163,7 +171,7 @@ export const useMany = < .data(pickedDataProvider) .resource(identifier) .action("many") - .ids(...ids) + .ids(...(ids ?? [])) .params({ ...(preferredMeta || {}), }) @@ -193,6 +201,7 @@ export const useMany = < ), ); }, + enabled: hasIds && hasResource, ...queryOptions, onSuccess: (data) => { queryOptions?.onSuccess?.(data); @@ -238,3 +247,14 @@ export const useMany = < return { ...queryResponse, overtime: { elapsedTime } }; }; + +const idsWarningMessage = ( + ids: BaseKey[], + resource: string, +) => `[useMany]: Missing "ids" prop. Expected an array of ids, but got "${typeof ids}". Resource: "${resource}" + +See https://refine.dev/docs/data/hooks/use-many/#ids-`; + +const resourceWarningMessage = () => `[useMany]: Missing "resource" prop. Expected a string, but got undefined. + +See https://refine.dev/docs/data/hooks/use-many/#resource-`;