Search code examples
typescriptreduxredux-toolkitrtk-query

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


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,
  };
},

Solution

  • 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 );