Skip to content

Commit b57e389

Browse files
authored
Refactor fetch logic (#57)
1 parent e3b7cc8 commit b57e389

File tree

5 files changed

+66
-121
lines changed

5 files changed

+66
-121
lines changed

packages/trpc-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "trpc-ui",
3-
"version": "1.0.10",
3+
"version": "1.0.11",
44
"description": "UI for testing tRPC backends",
55
"main": "lib/index.js",
66
"module": "lib/index.mjs",

packages/trpc-ui/src/react-app/Root.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ function ClientProviders({
9494
function AppInnards({
9595
rootRouter,
9696
options,
97-
}: { rootRouter: ParsedRouter; options: RenderOptions }) {
97+
}: {
98+
rootRouter: ParsedRouter;
99+
options: RenderOptions;
100+
}) {
98101
const [sidebarOpen, setSidebarOpen] = useLocalStorage(
99102
"trpc-panel.show-minimap",
100103
true,

packages/trpc-ui/src/react-app/components/form/ProcedureForm/index.tsx

Lines changed: 36 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import { ToggleJsonIcon } from "@src/react-app/components/icons/ToggleJsonIcon";
1212
import { trpc } from "@src/react-app/trpc";
1313
import type { RenderOptions } from "@src/render";
1414
import { sample } from "@stoplight/json-schema-sampler";
15-
import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
15+
1616
import { fullFormats } from "ajv-formats/dist/formats";
17-
import React, { useEffect, useRef, useState } from "react";
17+
import React, { useRef, useState } from "react";
1818
import { type Control, useForm, useFormState } from "react-hook-form";
1919
import getSize from "string-byte-length";
2020
import SuperJson from "superjson";
2121
import { z } from "zod";
22+
import { useAsyncDuration } from "../../hooks/useAsyncDuration";
2223
import { AutoFillIcon } from "../../icons/AutoFillIcon";
2324
import JSONEditor from "../JSONEditor";
2425
import { ErrorDisplay as ErrorComponent } from "./Error";
@@ -57,6 +58,16 @@ function isTrpcError(error: unknown): error is TRPCErrorType {
5758

5859
export const ROOT_VALS_PROPERTY_NAME = "vals";
5960

61+
// Recurse down the path to get the function to call
62+
function getUtilsOrProcedure(base: any, procedure: ParsedProcedure) {
63+
let cur = base;
64+
for (const p of procedure.pathFromRootRouter) {
65+
//@ts-ignore
66+
cur = cur[p];
67+
}
68+
return cur;
69+
}
70+
6071
export function ProcedureForm({
6172
procedure,
6273
options,
@@ -68,54 +79,12 @@ export function ProcedureForm({
6879
}) {
6980
// null => request was never sent
7081
// undefined => request successful but nothing returned from procedure
71-
const [mutationResponse, setMutationResponse] = useState<any>(null);
72-
const [queryEnabled, setQueryEnabled] = useState<boolean>(false);
73-
const [queryInput, setQueryInput] = useState<any>(null);
82+
const [response, setResponse] = useState<any>(null);
83+
const { duration, loading, measureAsyncDuration } = useAsyncDuration();
7484
const formRef = useRef<HTMLFormElement | null>(null);
75-
const context = trpc.useContext();
76-
const [startTime, setStartTime] = useState<number | undefined>();
77-
const [opDuration, setOpDuration] = useState<number | undefined>();
78-
79-
function getProcedure() {
80-
let cur: typeof trpc | (typeof trpc)[string] = trpc;
81-
for (const p of procedure.pathFromRootRouter) {
82-
// TODO - Maybe figure out these typings?
83-
//@ts-ignore
84-
cur = cur[p];
85-
}
86-
return cur;
87-
}
88-
89-
const query = (() => {
90-
const router = getProcedure();
91-
//@ts-ignore
92-
return router.useQuery(queryInput, {
93-
enabled: queryEnabled,
94-
initialData: null,
95-
retry: false,
96-
refetchOnWindowFocus: false,
97-
});
98-
})() as UseQueryResult<any>;
99-
100-
function invalidateQuery(input: any) {
101-
let cur: any = context;
102-
for (const p of procedure.pathFromRootRouter) {
103-
cur = cur[p];
104-
}
105-
cur.invalidate(input);
106-
}
107-
108-
const mutation = (() => {
109-
const router = getProcedure();
110-
//@ts-ignore
111-
return router.useMutation({
112-
retry: false,
113-
onSuccess: (data: unknown) => {
114-
if (startTime) setOpDuration(Date.now() - startTime);
115-
setStartTime(undefined);
116-
},
117-
});
118-
})() as UseMutationResult<any>;
85+
const utils = trpc.useUtils();
86+
const { mutateAsync } = getUtilsOrProcedure(trpc, procedure).useMutation();
87+
const fetchFunction = getUtilsOrProcedure(utils, procedure).fetch;
11988

12089
const {
12190
control,
@@ -131,74 +100,25 @@ export function ProcedureForm({
131100
[ROOT_VALS_PROPERTY_NAME]: defaultFormValuesForNode(procedure.node),
132101
},
133102
});
134-
function onSubmit(data: { [ROOT_VALS_PROPERTY_NAME]: any }) {
103+
async function onSubmit(data: { [ROOT_VALS_PROPERTY_NAME]: any }) {
135104
let newData: any;
136105
if (options.transformer === "superjson") {
137106
newData = SuperJson.serialize(data[ROOT_VALS_PROPERTY_NAME]);
138107
} else {
139108
newData = { ...data[ROOT_VALS_PROPERTY_NAME] };
140109
}
141-
if (procedure.procedureType === "query") {
142-
setQueryInput(newData);
143-
setQueryEnabled(true);
144-
invalidateQuery(newData);
145-
} else {
146-
setStartTime(Date.now());
147-
mutation.mutateAsync(newData).then(setMutationResponse).catch();
148-
}
110+
const apiCaller =
111+
procedure.procedureType === "query" ? fetchFunction : mutateAsync;
112+
const result = await measureAsyncDuration(
113+
async () => await apiCaller(newData),
114+
);
115+
setResponse(result);
149116
}
150117

151-
// I've seen stuff online saying form reset should happen in useEffect hook only
152-
// not really sure though, gonna just leave it for now
153-
const [shouldReset, setShouldReset] = useState(false);
154-
useEffect(() => {
155-
if (shouldReset) {
156-
resetForm(
157-
{ [ROOT_VALS_PROPERTY_NAME]: defaultFormValuesForNode(procedure.node) },
158-
{
159-
keepValues: false,
160-
keepDirtyValues: false,
161-
keepDefaultValues: false,
162-
},
163-
);
164-
setShouldReset(false);
165-
}
166-
}, [shouldReset, setShouldReset, resetForm, defaultFormValuesForNode]);
167-
function reset() {
168-
setShouldReset(true);
169-
setQueryEnabled(false);
170-
}
171-
172-
let data: any;
173-
if (procedure.procedureType === "query") {
174-
data = query.data ?? null;
175-
} else {
176-
data = mutationResponse;
177-
}
178-
179-
// Get raw size before deserialization
180-
const size = getSize(JSON.stringify(data));
181-
if (options.transformer === "superjson" && data) {
182-
data = SuperJson.deserialize(data);
183-
}
184-
const error =
185-
procedure.procedureType === "query" ? query.error : mutation.error;
186-
187-
// Fixes the timing for queries, not ideal but works
188-
useEffect(() => {
189-
if (query.fetchStatus === "fetching") {
190-
setStartTime(Date.now());
191-
}
192-
if (query.fetchStatus === "idle") {
193-
setOpDuration(Date.now() - startTime);
194-
}
195-
}, [query.fetchStatus]);
196-
197118
const fieldName = procedure.node.path.join(".");
198119

199120
const [useRawInput, setUseRawInput] = useState(false);
200121
function toggleRawInput() {
201-
console.log(getValues());
202122
setUseRawInput(!useRawInput);
203123
}
204124

@@ -226,7 +146,7 @@ export function ProcedureForm({
226146
title="Input"
227147
topRightElement={
228148
<div className="flex space-x-1">
229-
<XButton control={control} reset={reset} />
149+
<XButton control={control} reset={resetForm} />
230150
<div className="h-6 w-6">
231151
<button
232152
type="button"
@@ -269,25 +189,22 @@ export function ProcedureForm({
269189
<ProcedureFormButton
270190
text={`Execute ${name}`}
271191
colorScheme={"neutral"}
272-
loading={query.fetchStatus === "fetching" || mutation.isPending}
192+
loading={loading}
273193
/>
274194
</FormSection>
275195
</div>
276196
</form>
277197
<div className="flex flex-col space-y-4">
278-
{data && (
279-
<Response size={size} time={opDuration}>
280-
{data}
281-
</Response>
282-
)}
283-
{!data && data !== null && (
284-
<Response>Successful request but no data was returned</Response>
285-
)}
286-
{error &&
287-
(isTrpcError(error) ? (
288-
<ErrorComponent error={error} />
198+
{response &&
199+
(isTrpcError(response) ? (
200+
<ErrorComponent error={response} />
289201
) : (
290-
<Response>{error}</Response>
202+
<Response
203+
time={duration ?? undefined}
204+
size={getSize(JSON.stringify(response))}
205+
>
206+
{response}
207+
</Response>
291208
))}
292209
</div>
293210
</CollapsableSection>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useState } from "react";
2+
3+
export function useAsyncDuration() {
4+
const [duration, setDuration] = useState<number | null>(null);
5+
const [loading, setLoading] = useState(false);
6+
7+
const measureAsyncDuration = async <T,>(
8+
asyncFunction: () => Promise<T>,
9+
): Promise<T> => {
10+
setLoading(true);
11+
const startTime = performance.now();
12+
13+
try {
14+
const result = await asyncFunction();
15+
const endTime = performance.now();
16+
setDuration(endTime - startTime);
17+
return result;
18+
} finally {
19+
setLoading(false);
20+
}
21+
};
22+
23+
return { duration, loading, measureAsyncDuration };
24+
}

packages/trpc-ui/src/render.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const cache: {
6868
val: null,
6969
};
7070

71+
// TODO: changing this from AnyTRPCRouter to a generic type would probably improve type safety
7172
export function renderTrpcPanel(router: AnyTRPCRouter, options: RenderOptions) {
7273
if (options.cache === true && cache.val) return cache.val;
7374

0 commit comments

Comments
 (0)