Redux-Toolkit Query add params to query only if not null

84 Views Asked by At

I have query with several params, and I want to add params to query only if is not null.

Only thing I came up with create params object and conditionally add properties, but adding this for every query feels like wrong way, cause I think there is better way to handle with this My solution:

query: (category: string | null) => {
  const params: { [key: string]: string } = {};
  if (category !== null) params.category = category;

  return {
    url: "/products",
    params: params,
  };
},
2

There are 2 best solutions below

6
Ryan Pierce Williams On BEST ANSWER

You can write a general function that performs a Depth / Breadth First Traversal over the input object graph to modify (or copy + modify) it so that null properties are deleted or set to undefined.

I use a similar function in my code with RTK Query to perform serialization and deserialization to / from my back-end API.


EDIT: Here is a copy of my Depth First Traversal implementation for traversing a javascript object graph. It is generic and can be used for both pre- and post- traversals. I've written a number of unit tests for it in my own code and it works well.

import { isArray } from "lodash";

export const DefaultRootName = "[root]";

export interface DepthFirstTraversalOptions<TCustom = any> {
  // if true (default), then the algorithm will iterate over any and all array entries
  traverseArrayEntries: boolean; 

  // pre (default) = visit the root before the children. post = visit all the children and then the root.
  order: "pre" | "post"; 

  // If order = "pre", then this function is used to determine if the children will be visited after
  //  the parent node. This parameter will be ignored if order != "pre".
  skipChildren?: (parent: Readonly<any>, context: Readonly<VisitContext<TCustom>>) => boolean;

  // You may optionally provide a name for the root. This name will appear as the first entry within
  // context.path. This doesn't effect the functionality, but it maybe helpful for debugging.
  rootName?: string;
}

export interface VisitContext<TCustom = any> {
  path: string[];
  options: DepthFirstTraversalOptions;
  visited: Set<any>;
  visitStack: any[];
  custom: TCustom;
}

// Clone the source node, and apply any transformations as needed.
export type VisitFunction<TCustom = any> 
  = (current: Readonly<any>, context: Readonly<VisitContext<TCustom>>) => void;

/**
 * Performs a Depth First Traversal over all of the objects in the graph, starting from <source>.
 * Properties are not visited in any particular order.
 * @param source 
 * @param visit 
 * @param options 
 */
export function depthFirstTraversal<TCustom = any>(source: any, visit: VisitFunction<TCustom>, 
  custom?: TCustom, options?: Partial<DepthFirstTraversalOptions<TCustom>>) {

  const realOptions: DepthFirstTraversalOptions<TCustom> = {
    traverseArrayEntries: true,
    order: "pre",
    ...options
  };

  const visited = new Set<any>([source]); 
  const visitStack = [source];
  const path: string[] = [realOptions.rootName ?? DefaultRootName];

  const ctx: VisitContext = {
    path,
    options: realOptions,
    visited,
    visitStack,
    custom
  };

  __DepthFirstTraversal<TCustom>(source, visit, ctx);
}

// performs a depth-first traversal of the source object, visiting every property.
// First the root/source is visited and then its child properties, in no particular order. 
function __DepthFirstTraversal<TCustom = any>(source: any, visit: VisitFunction<TCustom>, context: VisitContext<TCustom>) {

  // assume that the context has already been updated prior to this internal call being made
  if (context.options.order === "pre") {
    visit(source, context);
    if (context.options.skipChildren?.(source, context)) {
      return;
    }
  }

  // check to see if the source is a primitive type. If so, we are done.
  // NOTE: the source could be undefined/null. 
  if (Object(source) !== source) { 
    if (context.options.order === "post") {
      visit(source, context);
    }
    return;
  }

  if (!context.options.traverseArrayEntries && isArray(source)) {
    if (context.options.order === "post") {
      visit(source, context);
    }
    return;
  }

  // visit any child nodes
  Object.keys(source).forEach(field => {
    const curr = source[field];

    if (context.visited.has(curr)) { 
      // We have already traversed through it via some other reference
      return; 
    } if (Object(curr) === curr) {
      // it is not a primitive, and this is our first time traversing to it 
      // register it to prevent re-iterating over the same object in the event that there
      // is a loop in the object graph.
      context.visited.add(curr);
    }

    context.visitStack.push(curr);
    context.path.push(field);
    __DepthFirstTraversal(curr, visit, context);
    context.path.pop();
    context.visitStack.pop();
  });

  if (context.options.order === "post") {
    visit(source, context);
  }
}

export default depthFirstTraversal;

If you wanted to use it to traverse an object graph and to modify all of the null properties to be undefined, then you could define a visit function like so (NOTE: This bit hasn't been tested / debugged, and may require special care for array entries):

const removeNullProperties:VisitFunction = 
(current, context) => {

  if(current !== null) { 
    return; 
  }

  // parent will be undefined if it is the root
  const parent = context.visitStack.at(-2); 
  const field = context.path.at(-1);

  parent[field] = undefined;
}; 

Alternatively, you could just delete the property like so: delete parent[field];

You can apply this with my earlier function to iterate over your input object graph (params) like so:

depthFirstTraversal( params, removeNullProperties );
1
Drew Reese On

An improvement might be to just pass the params object through directly from the calling code, since these appear to be your "args" anyway, and then you wouldn't need this extraneous check/logic in each endpoint, just pass in exactly what you want the query parameters to be. The base query function is capable of handling empty/undefined params.

Example:

getProducts: build.query</* ReturnType */, Record<string, any> | undefined>({
  query: (params) => {
    return {
      url: "/products",
      params
    }
  }
}),

or be more explicit per endpoint if you like:

getProducts: build.query</* ReturnType */, { category: string } | undefined>({
  query: (params) => {
    return {
      url: "/products",
      params
    }
  }
}),

Example Usage:

useGetProductsQuery(); // pass undefined
useGetProductsQuery({}); // pass empty params object
useGetProductsQuery({ category: "foo" }); // pass foo category