Skip to content

Commit 430449b

Browse files
committed
feat(code-gen): improve react-query DX by accepting a partial object on useQuery hooks
Accepts a partial object now on all `useQuery` hooks and related extensions. Validation of required arguments is already done via `queryOptions.enabled`. Also added runtime validation to catch issues when a custom `options.enabled` is used. The downside to this setup is that required arguments are not TS enforced. But that seems to be the most ergonomic setup, see for example https://tkdodo.eu/blog/react-query-and-type-script#type-safety-with-the-enabled-option
1 parent c2b4918 commit 430449b

File tree

1 file changed

+105
-35
lines changed

1 file changed

+105
-35
lines changed

packages/code-gen/src/api-client/react-query.js

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,11 @@ export function reactQueryGenerateFunction(
269269
}
270270

271271
// Helper variables for reusable patterns
272-
const joinedArgumentType = ({ withRequestConfig, withQueryOptions }) => {
272+
const joinedArgumentType = ({
273+
withRequestConfig,
274+
withQueryOptions,
275+
requireAllParams,
276+
}) => {
273277
const list = [
274278
contextNames.paramsTypeName,
275279
contextNames.queryTypeName,
@@ -299,7 +303,17 @@ export function reactQueryGenerateFunction(
299303
list.push(`{}`);
300304
}
301305

302-
return list.join(` & `);
306+
const result = list.join(` & `);
307+
308+
const requiredKeys = reactQueryGetRequiredFields(generateContext, route);
309+
310+
if (requiredKeys.length > 0 && !requireAllParams) {
311+
// We can just wrap it in an Partial which makes all required keys optional instead of writing them all out
312+
313+
return `Partial<${result}>`;
314+
}
315+
316+
return result;
303317
};
304318

305319
const parameterListWithExtraction = ({
@@ -458,6 +472,7 @@ export function reactQueryGenerateFunction(
458472
joinedArgumentType({
459473
withRequestConfig: true,
460474
withQueryOptions: true,
475+
requireAllParams: false,
461476
}),
462477
);
463478

@@ -487,20 +502,22 @@ export function reactQueryGenerateFunction(
487502
routeHasMandatoryInputs ? "opts" : ""
488503
}),`,
489504
);
490-
fileWriteInline(
505+
fileWrite(
491506
file,
492507
`({ signal }) => {
493-
opts.requestConfig ??= {};
494-
opts.requestConfig.signal = signal;
495-
496-
return ${apiName}(${apiInstanceParameter}
508+
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
509+
510+
opts.requestConfig ??= {};
511+
opts.requestConfig.signal = signal;
512+
513+
return ${apiName}(${apiInstanceParameter}
497514
${parameterListWithExtraction({
498515
prefix: "opts",
499516
withRequestConfig: true,
500517
defaultToNull: false,
501518
})}
502519
);
503-
}, options);`,
520+
}, options);`,
504521
);
505522

506523
fileBlockEnd(file);
@@ -521,6 +538,7 @@ ${hookName}.queryKey = (
521538
? `opts: ${joinedArgumentType({
522539
withQueryOptions: false,
523540
withRequestConfig: false,
541+
requireAllParams: false,
524542
})},`
525543
: ""
526544
}
@@ -542,18 +560,21 @@ ${hookName}.queryKey = (
542560
opts${routeHasMandatoryInputs ? "" : "?"}: ${joinedArgumentType({
543561
withQueryOptions: false,
544562
withRequestConfig: true,
563+
requireAllParams: false,
545564
})}
546565
) => {
547566
return queryClient.fetchQuery(
548567
${hookName}.queryKey(${routeHasMandatoryInputs ? "opts" : ""}),
549-
() => ${apiName}(
568+
() => {
569+
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
570+
return ${apiName}(
550571
${apiInstanceParameter}
551572
${parameterListWithExtraction({
552573
prefix: "opts",
553574
withRequestConfig: true,
554575
defaultToNull: false,
555576
})}
556-
));
577+
); });
557578
}
558579
559580
/**
@@ -565,18 +586,23 @@ ${hookName}.queryKey = (
565586
opts${routeHasMandatoryInputs ? "" : "?"}: ${joinedArgumentType({
566587
withQueryOptions: false,
567588
withRequestConfig: true,
589+
requireAllParams: false,
568590
})},
569591
) => {
570592
return queryClient.prefetchQuery(
571593
${hookName}.queryKey(${routeHasMandatoryInputs ? "opts" : ""}),
572-
() => ${apiName}(
594+
() => {
595+
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
596+
597+
return ${apiName}(
573598
${apiInstanceParameter}
574599
${parameterListWithExtraction({
575600
prefix: "opts",
576601
withRequestConfig: true,
577602
defaultToNull: false,
578603
})}
579-
));
604+
);
605+
});
580606
}
581607
582608
/**
@@ -589,6 +615,7 @@ ${hookName}.invalidate = (
589615
? `opts: ${joinedArgumentType({
590616
withQueryOptions: false,
591617
withRequestConfig: false,
618+
requireAllParams: false,
592619
})},`
593620
: ""
594621
}
@@ -607,14 +634,18 @@ ${hookName}.setQueryData = (
607634
? `opts: ${joinedArgumentType({
608635
withQueryOptions: false,
609636
withRequestConfig: false,
637+
requireAllParams: false,
610638
})},`
611639
: ""
612640
}
613641
data: ${contextNames.responseTypeName ?? "unknown"},
614-
) => queryClient.setQueryData(${hookName}.queryKey(${
642+
) => {
643+
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
644+
645+
return queryClient.setQueryData(${hookName}.queryKey(${
615646
routeHasMandatoryInputs ? "opts" : ""
616647
}), data);
617-
`,
648+
}`,
618649
);
619650
} else {
620651
// Write the props type
@@ -623,6 +654,7 @@ ${hookName}.setQueryData = (
623654
`type ${upperCaseFirst(hookName)}Props = ${joinedArgumentType({
624655
withRequestConfig: true,
625656
withQueryOptions: false,
657+
requireAllParams: true,
626658
})}`,
627659
);
628660

@@ -690,16 +722,71 @@ ${hookName}.setQueryData = (
690722
}
691723

692724
/**
693-
* Generate the api client hooks
725+
* Write out the dependencies for this query to be enabled
694726
*
695727
* @param {import("../generate.js").GenerateContext} generateContext
696728
* @param {import("../file/context.js").GenerateFile} file
697729
* @param {import("../../types/advanced-types").NamedType<import("../generated/common/types").StructureRouteDefinition>} route
698730
*/
699731
function reactQueryWriteIsEnabled(generateContext, file, route) {
732+
const keysAffectingEnabled = reactQueryGetRequiredFields(
733+
generateContext,
734+
route,
735+
);
736+
737+
if (keysAffectingEnabled.length > 0) {
738+
fileWrite(file, `options.enabled = (`);
739+
fileContextSetIndent(file, 1);
740+
741+
fileWrite(
742+
file,
743+
`options.enabled === true || (options.enabled !== false &&`,
744+
);
745+
fileWrite(
746+
file,
747+
keysAffectingEnabled
748+
.map((it) => `${it} !== undefined && ${it} !== null`)
749+
.join("&&\n"),
750+
);
751+
752+
fileContextSetIndent(file, -1);
753+
fileWrite(file, `));`);
754+
}
755+
}
756+
757+
/**
758+
* Write out the dependencies for this query to be enabled
759+
*
760+
* @param {import("../generate.js").GenerateContext} generateContext
761+
* @param {import("../../types/advanced-types").NamedType<import("../generated/common/types").StructureRouteDefinition>} route
762+
* @returns {string}
763+
*/
764+
function reactQueryCheckIfRequiredVariablesArePresent(generateContext, route) {
765+
const requiredFields = reactQueryGetRequiredFields(generateContext, route);
766+
767+
if (requiredFields.length > 0) {
768+
return `if (${requiredFields
769+
.map((it) => `${it} === undefined || ${it} === null`)
770+
.join("||\n")}) {
771+
throw new Error("Not all required variables where provided. This happens when you manually set 'queryOptions.enabled' or when you use 'refetch'. Both skip the generated 'queryOptions.enabled'. Make sure that all necessary arguments are set.");
772+
}
773+
`;
774+
}
775+
776+
return "";
777+
}
778+
779+
/**
780+
* Get the list of required fields.
781+
*
782+
* @param {import("../generate.js").GenerateContext} generateContext
783+
* @param {import("../../types/advanced-types").NamedType<import("../generated/common/types").StructureRouteDefinition>} route
784+
* @returns {string[]}
785+
*/
786+
function reactQueryGetRequiredFields(generateContext, route) {
700787
const keysAffectingEnabled = [];
701788

702-
for (const key of ["params", "query", "body"]) {
789+
for (const key of ["params", "query", "body", "files"]) {
703790
if (!route[key]) {
704791
continue;
705792
}
@@ -717,29 +804,12 @@ function reactQueryWriteIsEnabled(generateContext, file, route) {
717804
});
718805

719806
if (!isOptional) {
720-
keysAffectingEnabled.push(`opts.${subKey}`);
807+
keysAffectingEnabled.push(`opts["${subKey}"]`);
721808
}
722809
}
723810
}
724811

725-
if (keysAffectingEnabled.length > 0) {
726-
fileWrite(file, `options.enabled = (`);
727-
fileContextSetIndent(file, 1);
728-
729-
fileWrite(
730-
file,
731-
`options.enabled === true || (options.enabled !== false &&`,
732-
);
733-
fileWrite(
734-
file,
735-
keysAffectingEnabled
736-
.map((it) => `${it} !== undefined && ${it} !== null`)
737-
.join("&&\n"),
738-
);
739-
740-
fileContextSetIndent(file, -1);
741-
fileWrite(file, `));`);
742-
}
812+
return keysAffectingEnabled;
743813
}
744814

745815
/**

0 commit comments

Comments
 (0)