Skip to content

Commit

Permalink
Prevent loadQuery from being called in render
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller committed Nov 21, 2023
1 parent b84f0d5 commit 9a6c748
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 1 deletion.
70 changes: 69 additions & 1 deletion src/react/hooks/__tests__/useLoadableQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { InMemoryCache } from "../../../cache";
import { LoadableQueryHookFetchPolicy } from "../../types/types";
import { QueryReference } from "../../../react";
import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery";
import invariant from "ts-invariant";
import invariant, { InvariantError } from "ts-invariant";
import { profile, profileHook, spyOnConsole } from "../../../testing/internal";

interface SimpleQueryData {
Expand Down Expand Up @@ -3707,6 +3707,74 @@ it('does not suspend deferred queries with partial data in the cache and using a
}
});

it("throws when calling loadQuery on first render", async () => {
using _consoleSpy = spyOnConsole("error");
const { query, mocks } = useSimpleQueryCase();

function App() {
const [loadQuery] = useLoadableQuery(query);

loadQuery();

return null;
}

expect(() => renderWithMocks(<App />, { mocks })).toThrow(
new InvariantError(
"useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`."
)
);
});

it("throws when calling loadQuery on subsequent render", async () => {
using _consoleSpy = spyOnConsole("error");
const { query, mocks } = useSimpleQueryCase();

let error!: Error;

function App() {
const [count, setCount] = useState(0);
const [loadQuery] = useLoadableQuery(query);

if (count === 1) {
loadQuery();
}

return <button onClick={() => setCount(1)}>Load query in render</button>;
}

const { user } = renderWithMocks(
<ReactErrorBoundary onError={(e) => (error = e)} fallback={<div>Oops</div>}>
<App />
</ReactErrorBoundary>,
{ mocks }
);

await act(() => user.click(screen.getByText("Load query in render")));

expect(error).toEqual(
new InvariantError(
"useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`."
)
);
});

it("allows loadQuery to be called in useEffect on first render", async () => {
const { query, mocks } = useSimpleQueryCase();

function App() {
const [loadQuery] = useLoadableQuery(query);

React.useEffect(() => {
loadQuery();
}, []);

return null;
}

expect(() => renderWithMocks(<App />, { mocks })).not.toThrow();
});

describe.skip("type tests", () => {
it("returns unknown when TData cannot be inferred", () => {
const query = gql``;
Expand Down
14 changes: 14 additions & 0 deletions src/react/hooks/useLoadableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js";
import { canonicalStringify } from "../../cache/index.js";
import type { DeepPartial } from "../../utilities/index.js";
import type { CacheKey } from "../cache/types.js";
import { invariant } from "../../utilities/globals/index.js";

let RenderDispatcher: unknown = null;

type OnlyRequiredProperties<T> = {
[K in keyof T as {} extends Pick<T, K> ? never : K]: T[K];
Expand Down Expand Up @@ -113,6 +116,7 @@ export function useLoadableQuery<
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
options: LoadableQueryHookOptions = Object.create(null)
): UseLoadableQueryResult<TData, TVariables> {
RenderDispatcher = getRenderDispatcher();
const client = useApolloClient(options.client);
const suspenseCache = getSuspenseCache(client);
const watchQueryOptions = useWatchQueryOptions({ client, query, options });
Expand Down Expand Up @@ -178,6 +182,11 @@ export function useLoadableQuery<

const loadQuery: LoadQuery<TVariables> = React.useCallback(
(...args) => {
invariant(
getRenderDispatcher() !== RenderDispatcher,
"useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`."
);

const [variables] = args;

const cacheKey: CacheKey = [
Expand Down Expand Up @@ -207,3 +216,8 @@ export function useLoadableQuery<
];
}, [queryRef, loadQuery, fetchMore, refetch]);
}

function getRenderDispatcher() {
return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
?.ReactCurrentDispatcher?.current;
}

0 comments on commit 9a6c748

Please sign in to comment.