Custom Cache Key Resolver in Lodash Memoize

Lodash provides the _.memoize utility to optimize performance by caching the results of expensive function calls. By default, _.memoize determines its cache key using only the very first argument passed to the memoized function. This article explains how to override this default behavior by implementing a custom cache key resolver function, enabling proper caching for functions that accept multiple arguments or complex data types.

The Default Resolver Limitation

When using _.memoize(func), Lodash applies a default resolver that returns the first parameter (args[0]). If your target function relies on two or more parameters, calls sharing the same first argument will incorrectly return the cached result of the initial call, ignoring any subsequent arguments:

const add = (a, b) => a + b;
const memoizedAdd = _.memoize(add);

memoizedAdd(2, 3); // Returns 5 (computed)
memoizedAdd(2, 5); // Returns 5 (incorrect! Cache hit based only on '2')

Implementing a Custom Resolver

The _.memoize method accepts an optional second argument: resolver. The resolver is a function that receives all arguments passed to the memoized function and must return a value (usually a string or primitive) to be used as the internal cache key.

_.memoize(func, [resolver])

1. Combining Primitive Arguments

For functions that take multiple primitives (such as numbers or strings), you can join the arguments using a delimiter or template literals:

const add = (a, b) => a + b;

// Custom resolver combining both arguments
const memoizedAdd = _.memoize(
  add,
  (a, b) => `${a}_${b}`
);

memoizedAdd(2, 3); // Returns 5 (computed and cached under '2_3')
memoizedAdd(2, 5); // Returns 7 (computed and cached under '2_5')

2. Handling Dynamic Argument Lengths

If your function accepts a variable number of arguments, you can use the rest operator along with JSON.stringify or Array.prototype.join:

const multiplyAll = (...numbers) => numbers.reduce((acc, n) => acc * n, 1);

const memoizedMultiply = _.memoize(
  multiplyAll,
  (...args) => JSON.stringify(args)
);

memoizedMultiply(2, 3, 4); // Returns 24 (cached under '[2,3,4]')
memoizedMultiply(2, 3);    // Returns 6  (cached under '[2,3]')

3. Caching Based on Object Properties

When functions accept objects, generating a key from unique identifiers (such as an id) avoids serializing the entire object:

const fetchUserProfile = (user, options) => {
  // Expensive operation
  return `${user.name} - ${options.format}`;
};

const memoizedFetch = _.memoize(
  fetchUserProfile,
  (user, options) => `${user.id}:${options.format}`
);

If the object does not have a unique identifier, JSON.stringify(args) can be used, provided the object has no circular references and property order remains consistent.

Customizing the Cache Store

The generated key is stored in the function's .cache property, which by default is an instance of _.memoize.Cache (a Map-like object). You can inspect or modify cached entries directly using your custom key:

// Check if a key exists
memoizedAdd.cache.has('2_3'); // true

// Clear a specific entry
memoizedAdd.cache.delete('2_3');

// Clear the entire cache
memoizedAdd.cache.clear();