Skip to content

Commit

Permalink
feat(code-gen): improve react-query DX by accepting a partial object …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
dirkdev98 committed May 18, 2023
1 parent c2b4918 commit 430449b
Showing 1 changed file with 105 additions and 35 deletions.
140 changes: 105 additions & 35 deletions packages/code-gen/src/api-client/react-query.js
Expand Up @@ -269,7 +269,11 @@ export function reactQueryGenerateFunction(
}

// Helper variables for reusable patterns
const joinedArgumentType = ({ withRequestConfig, withQueryOptions }) => {
const joinedArgumentType = ({
withRequestConfig,
withQueryOptions,
requireAllParams,
}) => {
const list = [
contextNames.paramsTypeName,
contextNames.queryTypeName,
Expand Down Expand Up @@ -299,7 +303,17 @@ export function reactQueryGenerateFunction(
list.push(`{}`);
}

return list.join(` & `);
const result = list.join(` & `);

const requiredKeys = reactQueryGetRequiredFields(generateContext, route);

if (requiredKeys.length > 0 && !requireAllParams) {
// We can just wrap it in an Partial which makes all required keys optional instead of writing them all out

return `Partial<${result}>`;
}

return result;
};

const parameterListWithExtraction = ({
Expand Down Expand Up @@ -458,6 +472,7 @@ export function reactQueryGenerateFunction(
joinedArgumentType({
withRequestConfig: true,
withQueryOptions: true,
requireAllParams: false,
}),
);

Expand Down Expand Up @@ -487,20 +502,22 @@ export function reactQueryGenerateFunction(
routeHasMandatoryInputs ? "opts" : ""
}),`,
);
fileWriteInline(
fileWrite(
file,
`({ signal }) => {
opts.requestConfig ??= {};
opts.requestConfig.signal = signal;
return ${apiName}(${apiInstanceParameter}
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
opts.requestConfig ??= {};
opts.requestConfig.signal = signal;
return ${apiName}(${apiInstanceParameter}
${parameterListWithExtraction({
prefix: "opts",
withRequestConfig: true,
defaultToNull: false,
})}
);
}, options);`,
}, options);`,
);

fileBlockEnd(file);
Expand All @@ -521,6 +538,7 @@ ${hookName}.queryKey = (
? `opts: ${joinedArgumentType({
withQueryOptions: false,
withRequestConfig: false,
requireAllParams: false,
})},`
: ""
}
Expand All @@ -542,18 +560,21 @@ ${hookName}.queryKey = (
opts${routeHasMandatoryInputs ? "" : "?"}: ${joinedArgumentType({
withQueryOptions: false,
withRequestConfig: true,
requireAllParams: false,
})}
) => {
return queryClient.fetchQuery(
${hookName}.queryKey(${routeHasMandatoryInputs ? "opts" : ""}),
() => ${apiName}(
() => {
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
return ${apiName}(
${apiInstanceParameter}
${parameterListWithExtraction({
prefix: "opts",
withRequestConfig: true,
defaultToNull: false,
})}
));
); });
}
/**
Expand All @@ -565,18 +586,23 @@ ${hookName}.queryKey = (
opts${routeHasMandatoryInputs ? "" : "?"}: ${joinedArgumentType({
withQueryOptions: false,
withRequestConfig: true,
requireAllParams: false,
})},
) => {
return queryClient.prefetchQuery(
${hookName}.queryKey(${routeHasMandatoryInputs ? "opts" : ""}),
() => ${apiName}(
() => {
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
return ${apiName}(
${apiInstanceParameter}
${parameterListWithExtraction({
prefix: "opts",
withRequestConfig: true,
defaultToNull: false,
})}
));
);
});
}
/**
Expand All @@ -589,6 +615,7 @@ ${hookName}.invalidate = (
? `opts: ${joinedArgumentType({
withQueryOptions: false,
withRequestConfig: false,
requireAllParams: false,
})},`
: ""
}
Expand All @@ -607,14 +634,18 @@ ${hookName}.setQueryData = (
? `opts: ${joinedArgumentType({
withQueryOptions: false,
withRequestConfig: false,
requireAllParams: false,
})},`
: ""
}
data: ${contextNames.responseTypeName ?? "unknown"},
) => queryClient.setQueryData(${hookName}.queryKey(${
) => {
${reactQueryCheckIfRequiredVariablesArePresent(generateContext, route)}
return queryClient.setQueryData(${hookName}.queryKey(${
routeHasMandatoryInputs ? "opts" : ""
}), data);
`,
}`,
);
} else {
// Write the props type
Expand All @@ -623,6 +654,7 @@ ${hookName}.setQueryData = (
`type ${upperCaseFirst(hookName)}Props = ${joinedArgumentType({
withRequestConfig: true,
withQueryOptions: false,
requireAllParams: true,
})}`,
);

Expand Down Expand Up @@ -690,16 +722,71 @@ ${hookName}.setQueryData = (
}

/**
* Generate the api client hooks
* Write out the dependencies for this query to be enabled
*
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../file/context.js").GenerateFile} file
* @param {import("../../types/advanced-types").NamedType<import("../generated/common/types").StructureRouteDefinition>} route
*/
function reactQueryWriteIsEnabled(generateContext, file, route) {
const keysAffectingEnabled = reactQueryGetRequiredFields(
generateContext,
route,
);

if (keysAffectingEnabled.length > 0) {
fileWrite(file, `options.enabled = (`);
fileContextSetIndent(file, 1);

fileWrite(
file,
`options.enabled === true || (options.enabled !== false &&`,
);
fileWrite(
file,
keysAffectingEnabled
.map((it) => `${it} !== undefined && ${it} !== null`)
.join("&&\n"),
);

fileContextSetIndent(file, -1);
fileWrite(file, `));`);
}
}

/**
* Write out the dependencies for this query to be enabled
*
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../../types/advanced-types").NamedType<import("../generated/common/types").StructureRouteDefinition>} route
* @returns {string}
*/
function reactQueryCheckIfRequiredVariablesArePresent(generateContext, route) {
const requiredFields = reactQueryGetRequiredFields(generateContext, route);

if (requiredFields.length > 0) {
return `if (${requiredFields
.map((it) => `${it} === undefined || ${it} === null`)
.join("||\n")}) {
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.");
}
`;
}

return "";
}

/**
* Get the list of required fields.
*
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../../types/advanced-types").NamedType<import("../generated/common/types").StructureRouteDefinition>} route
* @returns {string[]}
*/
function reactQueryGetRequiredFields(generateContext, route) {
const keysAffectingEnabled = [];

for (const key of ["params", "query", "body"]) {
for (const key of ["params", "query", "body", "files"]) {
if (!route[key]) {
continue;
}
Expand All @@ -717,29 +804,12 @@ function reactQueryWriteIsEnabled(generateContext, file, route) {
});

if (!isOptional) {
keysAffectingEnabled.push(`opts.${subKey}`);
keysAffectingEnabled.push(`opts["${subKey}"]`);
}
}
}

if (keysAffectingEnabled.length > 0) {
fileWrite(file, `options.enabled = (`);
fileContextSetIndent(file, 1);

fileWrite(
file,
`options.enabled === true || (options.enabled !== false &&`,
);
fileWrite(
file,
keysAffectingEnabled
.map((it) => `${it} !== undefined && ${it} !== null`)
.join("&&\n"),
);

fileContextSetIndent(file, -1);
fileWrite(file, `));`);
}
return keysAffectingEnabled;
}

/**
Expand Down

0 comments on commit 430449b

Please sign in to comment.