-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve useMutation and useQuery signatures
- Loading branch information
Julio García
committed
Aug 10, 2021
1 parent
582c47c
commit 65357cc
Showing
11 changed files
with
3,046 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import "whatwg-fetch"; | ||
import "@testing-library/jest-dom"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/** @type {import('@ts-jest/dist/types').InitialOptionsTsJest} */ | ||
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "jsdom", | ||
setupFilesAfterEnv: ["<rootDir>/jest-setup.ts"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { default as useMutation } from "./useMutation"; | ||
export { default as useQuery } from "./useQuery"; | ||
export { default as wrapRootComponent } from "./wrapRootComponent"; | ||
export { invalidateQuery } from "./utils"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export type AnyFunction = (...args: any) => any; | ||
export type NoArgsFunction = () => any; | ||
|
||
export type FirstParamOrFallback< | ||
Func extends AnyFunction, | ||
Fallback, | ||
> = Func extends NoArgsFunction | ||
? Fallback | ||
: Func extends (firstParam: infer FirstParamType, ...args: any) => any | ||
? FirstParamType | ||
: never; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import * as React from "react"; | ||
import { fireEvent, render, screen } from "@testing-library/react"; | ||
import wrapRootComponent from "./wrapRootComponent"; | ||
import { setupServer } from "msw/node"; | ||
import { rest } from "msw"; | ||
import { useUnsweetenedMutation } from "./useMutation"; | ||
import { useUnsweetenedQuery } from "./useQuery"; | ||
import { QueryClient, useQueryClient as useRQQueryClient } from "react-query"; | ||
import { AnyFunction } from "./ts-helpers"; | ||
|
||
interface ExtendedQueryClient extends QueryClient { | ||
invalidateQuery: (queryKey: AnyFunction) => void; | ||
invalidateQueryKey: (queryKey: string) => void; | ||
} | ||
|
||
function useQueryClient( | ||
...args: Parameters<typeof useRQQueryClient> | ||
): ExtendedQueryClient { | ||
const client = useRQQueryClient(...args) as ExtendedQueryClient; | ||
|
||
client.invalidateQueryKey = function (queryKey: string) { | ||
this.invalidateQueries(queryKey); | ||
}; | ||
client.invalidateQuery = function (queryKey: AnyFunction) { | ||
// This is not a arbitrary convertion. queryKey works differently between the TS world | ||
// and the runtime world: | ||
// - In the TS world, this is a function | ||
// - In the runtime world, this is a string | ||
const queryKeyAsString = queryKey as unknown as string; | ||
this.invalidateQueryKey(queryKeyAsString); | ||
}; | ||
|
||
client.invalidateQueryKey = client.invalidateQueryKey.bind(client); | ||
client.invalidateQuery = client.invalidateQuery.bind(client); | ||
|
||
return client; | ||
} | ||
|
||
let serverData: { numbers: Array<number> } = { | ||
numbers: [], | ||
}; | ||
|
||
const server = setupServer( | ||
rest.post("/abledev/call-mutation", (req, res, ctx) => { | ||
const key = req.url.searchParams.get("key"); | ||
|
||
if (key === "mutations/push-next") { | ||
const { numbers } = serverData; | ||
serverData.numbers.push((numbers[numbers.length - 1] ?? 0) + 1); | ||
} | ||
|
||
if (key === "mutations/push") { | ||
const newNumber = (req.body as any).number as number; | ||
serverData.numbers.push(newNumber); | ||
} | ||
|
||
return res(ctx.json({})); | ||
}), | ||
rest.get("/abledev/call-query", (_req, res, ctx) => { | ||
return res(ctx.json(serverData.numbers)); | ||
}), | ||
); | ||
|
||
beforeAll(() => server.listen()); | ||
afterEach(() => server.resetHandlers()); | ||
afterAll(() => server.close()); | ||
|
||
const TestComponent = wrapRootComponent(() => { | ||
const { isLoading, data } = useUnsweetenedQuery("queries/get-all"); | ||
const { invalidateQueryKey } = useQueryClient(); | ||
|
||
const pushNextMutation = useUnsweetenedMutation("mutations/push-next", { | ||
onSettled: () => { | ||
invalidateQueryKey("queries/get-all"); | ||
}, | ||
}); | ||
const pushMutation = useUnsweetenedMutation("mutations/push", { | ||
onSettled: () => { | ||
invalidateQueryKey("queries/get-all"); | ||
}, | ||
}); | ||
|
||
const [newNumber, setNewNumber] = React.useState(99); | ||
|
||
return ( | ||
<> | ||
<button onClick={() => pushNextMutation.mutate({})}>Push Next</button> | ||
|
||
<input | ||
type="text" | ||
value={newNumber.toString()} | ||
onChange={(event) => { | ||
setNewNumber(Number(event.target.value)); | ||
}} | ||
placeholder="New Number" | ||
/> | ||
<button onClick={() => pushMutation.mutate({ number: newNumber })}> | ||
Push | ||
</button> | ||
<div> | ||
{isLoading ? ( | ||
<span>Loading...</span> | ||
) : ( | ||
<span>{JSON.stringify(data as object)}</span> | ||
)} | ||
</div> | ||
</> | ||
); | ||
}); | ||
|
||
test("It calls a server and updates something", async () => { | ||
render(<TestComponent />); | ||
fireEvent.click(screen.getByRole("button", { name: "Push Next" })); | ||
fireEvent.click(screen.getByRole("button", { name: "Push Next" })); | ||
expect(await screen.findByText("[1,2]")); | ||
}); | ||
|
||
test("It can receive some arguments", async () => { | ||
serverData.numbers = []; | ||
|
||
render(<TestComponent />); | ||
fireEvent.change(screen.getByPlaceholderText("New Number"), { | ||
target: { value: "199" }, | ||
}); | ||
fireEvent.click(screen.getByRole("button", { name: "Push" })); | ||
expect(await screen.findByText("[199]")); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,63 @@ | ||
import { useMutation as useRQMutation } from "react-query"; | ||
import { useMutation as useRQMutation, UseMutationOptions } from "react-query"; | ||
import { AnyFunction, FirstParamOrFallback } from "./ts-helpers"; | ||
|
||
type AnyFunction = (...args: any) => any; | ||
type OptionsFromResultAndArgsTypes<ResultType, ArgumentsType> = | ||
UseMutationOptions<ResultType, unknown, ArgumentsType, string>; | ||
|
||
type OptionsFromMutationKey<MutationKey extends AnyFunction> = | ||
OptionsFromResultAndArgsTypes< | ||
ReturnType<MutationKey>, | ||
FirstParamOrFallback<MutationKey, object> | ||
>; | ||
|
||
function useMutation<MutationKey extends AnyFunction>( | ||
mutationKey: MutationKey, | ||
mutationConfiguration: OptionsFromMutationKey<MutationKey> = {}, | ||
) { | ||
// This is not a arbitrary convertion. queryKey works differently between the TS world | ||
// and the runtime world: | ||
// - In the TS world, this is a function | ||
// - In the runtime world, this is a string | ||
const mutationKeyAsString = mutationKey as unknown as string; | ||
|
||
return useRQMutation(mutationKeyAsString, async (args: object = {}) => { | ||
const url = new URL(`${location.origin}/abledev/call-mutation`); | ||
url.search = new URLSearchParams({ key: mutationKeyAsString }).toString(); | ||
return fetch(url.toString(), { | ||
method: "POST", | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify(args), | ||
}).then((response) => { | ||
return response.json() as ReturnType<MutationKey>; | ||
}); | ||
}); | ||
return useUnsweetenedMutation< | ||
ReturnType<MutationKey>, | ||
FirstParamOrFallback<MutationKey, object> | ||
>(mutationKeyAsString, mutationConfiguration); | ||
} | ||
|
||
function useUnsweetenedMutation<ResultType, ArgumentsType>( | ||
mutationKey: string, | ||
mutationConfiguration: OptionsFromResultAndArgsTypes< | ||
ResultType, | ||
ArgumentsType | ||
> = {}, | ||
) { | ||
return useRQMutation<ResultType, unknown, ArgumentsType, string>( | ||
mutationKey, | ||
async (mutationArguments) => { | ||
const url = new URL(`${location.origin}/abledev/call-mutation`); | ||
url.search = new URLSearchParams({ key: mutationKey }).toString(); | ||
|
||
try { | ||
const response = await fetch(url.toString(), { | ||
method: "POST", | ||
headers: { | ||
Accept: "application/json", | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify(mutationArguments), | ||
}); | ||
const responseData = await response.json(); | ||
return responseData as unknown as ResultType; | ||
} catch (error) { | ||
throw error; | ||
} | ||
}, | ||
mutationConfiguration, | ||
); | ||
} | ||
|
||
export { useUnsweetenedMutation }; | ||
|
||
export default useMutation; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import * as React from "react"; | ||
import { render, screen } from "@testing-library/react"; | ||
import { useUnsweetenedQuery } from "./useQuery"; | ||
import wrapRootComponent from "./wrapRootComponent"; | ||
import { setupServer } from "msw/node"; | ||
import { rest } from "msw"; | ||
|
||
const server = setupServer( | ||
rest.get("/abledev/call-query", (req, res, ctx) => { | ||
const response: any = { key: req.url.searchParams.get("key") }; | ||
if (req.url.searchParams.get("name")) { | ||
response.name = req.url.searchParams.get("name"); | ||
} | ||
return res(ctx.json(response)); | ||
}), | ||
); | ||
|
||
beforeAll(() => server.listen()); | ||
afterEach(() => server.resetHandlers()); | ||
afterAll(() => server.close()); | ||
|
||
const TestComponent = wrapRootComponent(() => { | ||
const { isLoading, data } = useUnsweetenedQuery("queries/something"); | ||
|
||
return isLoading ? ( | ||
<span>loading...</span> | ||
) : ( | ||
<> | ||
<span>done</span> | ||
<span>{JSON.stringify(data as object)}</span> | ||
</> | ||
); | ||
}); | ||
|
||
const ParameterizedComponent = wrapRootComponent( | ||
({ name }: { name: string }) => { | ||
const { isLoading, data } = useUnsweetenedQuery("queries/something", { | ||
name, | ||
}); | ||
|
||
return isLoading ? ( | ||
<span>loading...</span> | ||
) : ( | ||
<> | ||
<span>done</span> | ||
<span>{JSON.stringify(data as object)}</span> | ||
</> | ||
); | ||
}, | ||
); | ||
|
||
test("It calls a server", async () => { | ||
render(<TestComponent />); | ||
expect(screen.getByText("loading...")).toBeInTheDocument(); | ||
expect(await screen.findByText("done")).toBeInTheDocument(); | ||
expect(await screen.findByText(/queries\/something/)).toBeInTheDocument(); | ||
}); | ||
|
||
test("It can receive some arguments", async () => { | ||
render(<ParameterizedComponent name="Rare name" />); | ||
expect(await screen.findByText(/Rare name/)).toBeInTheDocument(); | ||
}); |
Oops, something went wrong.