Lodash Memoize with Multiple Arguments
By default, the Lodash _.memoize function only utilizes
the first argument passed to a memoized function to create its cache
key, ignoring any subsequent arguments. When memoizing functions that
accept multiple parameters, this behavior causes inaccurate cache hits
and stale returns. To correctly cache results based on all supplied
inputs, you must pass a custom resolver function as the second argument
to _.memoize that serializes or maps every argument into a
unique identifier.
The Default Behavior Issue
Consider a function that adds two numbers:
const add = (a, b) => a + b;
const memoizedAdd = _.memoize(add);
memoizedAdd(1, 2); // Returns 3, caches under key: 1
memoizedAdd(1, 5); // Returns 3, because cache key 1 already existsBecause Lodash defaults to using arguments[0] as the
cache key, memoizedAdd(1, 5) mistakenly returns the cached
result of memoizedAdd(1, 2).
Implementing a Custom Resolver
The _.memoize function accepts a second argument,
resolver, which receives all arguments passed to the
memoized function:
_.memoize(func, [resolver]);The resolver must return a single value (typically a primitive like a
string) to act as the cache key in Lodash's internal
Map.
1. The JSON.stringify
Approach
The most common and versatile solution is to serialize the entire
argument list into a JSON string using JSON.stringify:
const add = (a, b) => a + b;
const memoizedAdd = _.memoize(
add,
(...args) => JSON.stringify(args)
);
memoizedAdd(1, 2); // Caches under key: "[1,2]" -> Returns 3
memoizedAdd(1, 5); // Caches under key: "[1,5]" -> Returns 6This approach works effectively for primitives, plain arrays, and serializable objects.
2. Delimited String Joining
For functions that strictly receive primitive values (such as numbers
or clean strings), converting the argument array directly to a joined
string offers better performance than JSON.stringify:
const getUserPermissions = (userId, role) => {
// Expensive lookup
return `${userId}:${role}`;
};
const memoizedPermissions = _.memoize(
getUserPermissions,
(userId, role) => `${userId}__${role}`
);When using delimiters, ensure the chosen separator cannot appear
within the input values themselves to prevent key collisions (for
example, inputs ('a_b', 'c') and ('a', 'b_c')
would both produce 'a_b_c' if using an underscore
delimiter).
Considerations and Limitations
When creating a custom resolver for multiple arguments, consider the following constraints:
- Object Key Ordering:
JSON.stringifyrelies on the insertion order of object keys.{ a: 1, b: 2 }and{ b: 2, a: 1 }generate different strings and will result in cache misses even if the objects are functionally identical. - Non-Serializable Values: Functions,
undefined,NaN, and circular references do not serialize predictably withJSON.stringify. If your arguments contain non-serializable references, you must write a dedicated resolver that maps references to unique IDs or symbols. - Performance Overhead: Complex serialization for high-frequency operations can negate the performance benefits of caching. In performance-critical paths, prefer strict primitive keys over serializing large objects.